From 3a7c86f1d46faaf446da7ab0cc16b6657a984f5f Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Mon, 3 Aug 2026 19:37:15 -0300 Subject: [PATCH 01/51] feat(ngmp): setup branch and instruction context for generals online --- .github/instructions/ngmp.instructions.md | 34 +++++++++++++++++++++++ AGENTS.md | 1 + docs/WORKLOG/2026-08-DIARY.md | 8 ++++++ 3 files changed, 43 insertions(+) create mode 100644 .github/instructions/ngmp.instructions.md create mode 100644 docs/WORKLOG/2026-08-DIARY.md diff --git a/.github/instructions/ngmp.instructions.md b/.github/instructions/ngmp.instructions.md new file mode 100644 index 00000000000..f602e27256b --- /dev/null +++ b/.github/instructions/ngmp.instructions.md @@ -0,0 +1,34 @@ +--- +applyTo: '**/GeneralsOnline/**,**/NextGenMP/**' +--- + +# NGMP Subsystem Implementation Instructions + +These instructions govern the Next-Gen Multiplayer (NGMP) client protocol integration into **GeneralsX**. All changes in `GeneralsOnline` and `NextGenMP` components must adhere strictly to these guidelines. + +--- + +## Golden Constraints + +1. **Strict Prohibition of `` & Win32 APIs**: + - Never include ``, ``, ``, ``, or `` in NGMP sources. + - Use standard C++ (``, ``, ``), POSIX networking (``, ``, ``), or SDL3 primitives (`SDL_GetTicks()`, `SDL_Delay()`, `SDL_GetPrefPath()`). + +2. **Platform Layer Isolation**: + - Low-level network socket and OS calls must reside exclusively in `Core/GameEngineDevice/`. + - Higher-level network logic under `GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/` must use pure abstract interfaces and cross-platform abstractions. + +3. **Thread Safety & UI Main Thread Separation**: + - Network callbacks from HTTP/WebSocket/P2P threads **MUST NOT** directly access or mutate UI controls (`GameWindow`, `WOLLobbyMenu`, etc.). + - Network events must be posted to a thread-safe thread/FIFO queue, consumed strictly on the main render thread during `Shell::update()`. + +4. **Credential & Token Storage**: + - Do NOT use Windows Credential Manager. + - Save session JWT tokens in an obfuscated local configuration file under the directory provided by `SDL_GetPrefPath("GeneralsX", "GeneralsOnline")`. + +5. **Server Endpoint Environment**: + - Development backend server target: `ws://192.168.1.120:9001/ws` (WebSocket) and `http://192.168.1.120:9001/api` (REST). + - Server URLs must be configurable via INI/JSON config rather than hardcoded string literals. + +6. **Cross-Platform Math & Endianness**: + - Ensure network packet data serialization handles network byte order (`htons`/`ntohs`, `htonl`/`ntohl`) explicitly to support cross-play between x86_64 Linux and ARM64 macOS. diff --git a/AGENTS.md b/AGENTS.md index 854ac51eb36..cd42974936f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -288,5 +288,6 @@ The `**` at applyTo means all files, you MUST load it everytime. | [.github/instructions/platform-macos.instructions.md](.github/instructions/platform-macos.instructions.md) | `scripts/build/macos/**,references/fbraz3-dxvk/**` | macOS/DXVK build notes | | [.github/instructions/docs.instructions.md](.github/instructions/docs.instructions.md) | `**/*.md` | Documentation structure and workflow | | [.github/instructions/scripts.instructions.md](.github/instructions/scripts.instructions.md) | `scripts/**` | Script organization and naming | +| [.github/instructions/ngmp.instructions.md](.github/instructions/ngmp.instructions.md) | `**/GeneralsOnline/**,**/NextGenMP/**` | NGMP cross-platform multiplayer integration guidelines | Update this table when instruction files are added, removed, or renamed. diff --git a/docs/WORKLOG/2026-08-DIARY.md b/docs/WORKLOG/2026-08-DIARY.md new file mode 100644 index 00000000000..7afe8b0d01d --- /dev/null +++ b/docs/WORKLOG/2026-08-DIARY.md @@ -0,0 +1,8 @@ +# August 2026 Development Diary + +## 2026-08-03 + +### Phase 1: GeneralsOnline NGMP Setup +- Created feature branch `feat/generals-online-ngmp` off `main`. +- Added dedicated subsystem instructions in `.github/instructions/ngmp.instructions.md` with constraints on Windows OS API removal, POSIX/SDL3 abstractions, thread-safe UI queues, and default server endpoint (`ws://192.168.1.120:9001/ws`). +- Registered `ngmp.instructions.md` in `AGENTS.md` instruction context loading table. From 2d20e87364570eb34ffa6185f3ac200188a0f7b4 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Mon, 3 Aug 2026 20:03:32 -0300 Subject: [PATCH 02/51] feat(ngmp): implement core ngmp online services manager and ui hooks --- CMakeLists.txt | 3 + GeneralsMD/Code/GameEngine/CMakeLists.txt | 6 + .../GameNetwork/GeneralsOnline/NGMP_Helpers.h | 35 +++++ .../GeneralsOnline/OnlineServices_Manager.h | 75 +++++++++++ .../GameEngine/Source/Common/GameEngine.cpp | 12 ++ .../GeneralsOnline/NGMP_Helpers.cpp | 72 ++++++++++ .../GeneralsOnline/OnlineServices_Auth.cpp | 119 +++++++++++++++++ .../GeneralsOnline/OnlineServices_Init.cpp | 36 +++++ .../GeneralsOnline/OnlineServices_Manager.cpp | 123 ++++++++++++++++++ cmake/config-build.cmake | 9 ++ cmake/ngmp.cmake | 27 ++++ docs/WORKLOG/2026-08-DIARY.md | 9 ++ 12 files changed, 526 insertions(+) create mode 100644 GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h create mode 100644 GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h create mode 100644 GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp create mode 100644 GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp create mode 100644 GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp create mode 100644 GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp create mode 100644 cmake/ngmp.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 98bc8ca8bde..f425aa8dd57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,9 @@ include(cmake/miniaudio.cmake) # curl.cmake is self-guarded with if(SAGE_UPDATE_CHECK), so always safe to include. include(cmake/curl.cmake) +# GeneralsX @feature GeneralsOnline NGMP protocol dependency setup +include(cmake/ngmp.cmake) + # GeneralsX @feature fbraz 03/05/2026 Phase 4: Deterministic math library integration # gamemath.cmake integrates fdlibm-based GameMath for cross-platform replay validation. # Upstream reference: Okladnoj, PR #2670 diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 924e981b3d1..b25c58b2452 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -1111,6 +1111,12 @@ set(GAMEENGINE_SRC # Source/GameNetwork/GameSpy/Thread/ThreadUtils.cpp # Source/GameNetwork/GameSpyOverlay.cpp Source/GameNetwork/GUIUtil.cpp + Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h + Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h + Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp + Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp + Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp + Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp # Source/GameNetwork/IPEnumeration.cpp # Source/GameNetwork/LANAPI.cpp # Source/GameNetwork/LANAPICallbacks.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h new file mode 100644 index 00000000000..6ca1b7c2926 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h @@ -0,0 +1,35 @@ +// GeneralsX @feature GeneralsOnline NGMP Helpers header +// Cross-platform abstraction for OS primitives, timing, and storage. + +#ifndef NGMP_HELPERS_H +#define NGMP_HELPERS_H + +#include +#include + +namespace NGMP { + +// Returns time in milliseconds since application start using SDL3/chrono primitives +uint32_t GetTicks(); + +// Pauses execution for the specified milliseconds +void Delay(uint32_t ms); + +// Returns absolute user storage directory path for GeneralsOnline data +std::string GetStoragePath(); + +// Saves authentication token to local user storage +bool SaveAuthToken(const std::string& token); + +// Loads authentication token from local user storage +std::string LoadAuthToken(); + +// Returns default server WS endpoint URL +std::string GetServerWSEndpoint(); + +// Returns default server REST endpoint URL +std::string GetServerRESTEndpoint(); + +} // namespace NGMP + +#endif // NGMP_HELPERS_H diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h new file mode 100644 index 00000000000..0163b11774b --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h @@ -0,0 +1,75 @@ +// GeneralsX @feature GeneralsOnline NGMP OnlineServices Manager header +// Thread-safe manager for Next-Gen Multiplayer protocol lifecycle and event dispatching. + +#ifndef ONLINE_SERVICES_MANAGER_H +#define ONLINE_SERVICES_MANAGER_H + +#include +#include +#include +#include +#include + +struct NGMPEvent { + enum Type { + EVENT_NONE, + EVENT_AUTH_SUCCESS, + EVENT_AUTH_FAILURE, + EVENT_LOBBY_LIST_UPDATED, + EVENT_CHAT_MESSAGE_RECEIVED, + EVENT_DISCONNECTED + }; + + Type type = EVENT_NONE; + std::string payload; +}; + +struct NGMPLobby { + std::string id; + std::string name; + std::string mapName; + int currentPlayers = 0; + int maxPlayers = 8; +}; + +class NGMP_OnlineServicesManager { +public: + static NGMP_OnlineServicesManager& getInstance(); + + bool init(); + void update(); // Main thread UI tick dispatch + void shutdown(); + + bool login(const std::string& username, const std::string& password); + bool loginWithToken(const std::string& token); + void logout(); + + void requestLobbyList(); + bool sendChatMessage(const std::string& room, const std::string& message); + + bool isLoggedIn() const { return m_isLoggedIn; } + std::string getAuthToken() const { return m_authToken; } + std::string getUsername() const { return m_username; } + const std::vector& getLobbies() const { return m_lobbies; } + + // Internal thread-safe event poster + void postEvent(const NGMPEvent& event); + +private: + NGMP_OnlineServicesManager(); + ~NGMP_OnlineServicesManager(); + + NGMP_OnlineServicesManager(const NGMP_OnlineServicesManager&) = delete; + NGMP_OnlineServicesManager& operator=(const NGMP_OnlineServicesManager&) = delete; + + bool m_initialized = false; + bool m_isLoggedIn = false; + std::string m_username; + std::string m_authToken; + std::vector m_lobbies; + + mutable std::mutex m_eventMutex; + std::queue m_eventQueue; +}; + +#endif // ONLINE_SERVICES_MANAGER_H diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index 28446c80f52..2277bd21dc3 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -88,6 +88,10 @@ #include "GameClient/ClientInstance.h" #include "GameClient/FXList.h" #include "GameClient/GameClient.h" + +#ifdef SAGE_USE_NGMP +#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" +#endif #include "GameClient/Keyboard.h" #include "GameClient/Shell.h" #include "GameClient/GameText.h" @@ -688,6 +692,10 @@ void GameEngine::init() initSubsystem(TheUpgradeCenter,"TheUpgradeCenter", MSGNEW("GameEngineSubsystem") UpgradeCenter, &xferCRC, "Data\\INI\\Default\\Upgrade", "Data\\INI\\Upgrade"); initSubsystem(TheGameClient,"TheGameClient", createGameClient(), nullptr); +#ifdef SAGE_USE_NGMP + NGMP_OnlineServicesManager::getInstance().init(); +#endif + #ifdef DUMP_PERF_STATS/////////////////////////////////////////////////////////////////////////// GetPrecisionTimer(&endTime64);////////////////////////////////////////////////////////////////// @@ -1009,6 +1017,10 @@ void GameEngine::update() { TheNetwork->UPDATE(); } + +#ifdef SAGE_USE_NGMP + NGMP_OnlineServicesManager::getInstance().update(); +#endif } // TheSuperHackers @info Ignores frozen time because the script engine needs updating in the logic update regardless. diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp new file mode 100644 index 00000000000..edcbd951b98 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp @@ -0,0 +1,72 @@ +// GeneralsX @feature GeneralsOnline NGMP Helpers implementation +// Cross-platform OS abstraction using pure C++20 standard library. + +#include "GameNetwork/GeneralsOnline/NGMP_Helpers.h" +#include +#include +#include +#include +#include +#include + +namespace NGMP { + +static const char* DEFAULT_WS_ENDPOINT = "ws://192.168.1.120:9001/ws"; +static const char* DEFAULT_REST_ENDPOINT = "http://192.168.1.120:9001/api"; + +uint32_t GetTicks() { + auto now = std::chrono::steady_clock::now(); + return static_cast( + std::chrono::duration_cast(now.time_since_epoch()).count() + ); +} + +void Delay(uint32_t ms) { + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); +} + +std::string GetStoragePath() { + std::string baseDir; + const char* home = std::getenv("HOME"); + if (home) { + baseDir = std::string(home) + "/.generals_online/"; + } else { + baseDir = "./.generals_online/"; + } + return baseDir; +} + +bool SaveAuthToken(const std::string& token) { + std::string path = GetStoragePath(); + std::filesystem::create_directories(path); + std::string tokenFile = path + "session.token"; + std::ofstream out(tokenFile, std::ios::out | std::ios::trunc); + if (!out.is_open()) { + return false; + } + out << token; + out.close(); + return true; +} + +std::string LoadAuthToken() { + std::string path = GetStoragePath(); + std::string tokenFile = path + "session.token"; + std::ifstream in(tokenFile); + if (!in.is_open()) { + return ""; + } + std::string token; + in >> token; + return token; +} + +std::string GetServerWSEndpoint() { + return DEFAULT_WS_ENDPOINT; +} + +std::string GetServerRESTEndpoint() { + return DEFAULT_REST_ENDPOINT; +} + +} // namespace NGMP diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp new file mode 100644 index 00000000000..e376ae2fe54 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp @@ -0,0 +1,119 @@ +// GeneralsX @feature GeneralsOnline NGMP Auth implementation +// Handles user authentication and JWT session token persistence. + +#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" +#include "GameNetwork/GeneralsOnline/NGMP_Helpers.h" +#include +#include +#include + +using json = nlohmann::json; + +namespace { + struct CurlResponse { + std::string text; + }; + + size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { + size_t totalSize = size * nmemb; + CurlResponse* resp = static_cast(userp); + resp->text.append(static_cast(contents), totalSize); + return totalSize; + } +} + +bool NGMP_OnlineServicesManager::login(const std::string& username, const std::string& password) { + fprintf(stderr, "[NGMP] Attempting login for user: %s\n", username.c_str()); + fflush(stderr); + + CURL* curl = curl_easy_init(); + if (!curl) { + fprintf(stderr, "[NGMP] Failed to initialize libcurl for auth\n"); + fflush(stderr); + return false; + } + + std::string url = NGMP::GetServerRESTEndpoint() + "/auth/login"; + json requestJson = { + {"username", username}, + {"password", password} + }; + std::string requestBody = requestJson.dump(); + + CurlResponse response; + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, requestBody.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + + CURLcode res = curl_easy_perform(curl); + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (res == CURLE_OK && (httpCode == 200 || httpCode == 201)) { + try { + auto responseJson = json::parse(response.text); + if (responseJson.contains("token")) { + m_authToken = responseJson["token"].get(); + m_username = username; + m_isLoggedIn = true; + + NGMP::SaveAuthToken(m_authToken); + + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_AUTH_SUCCESS; + ev.payload = m_authToken; + postEvent(ev); + + fprintf(stderr, "[NGMP] Login successful for user: %s\n", username.c_str()); + fflush(stderr); + return true; + } + } catch (const std::exception& e) { + fprintf(stderr, "[NGMP] JSON parse exception during auth: %s\n", e.what()); + fflush(stderr); + } + } + + // Auth failed + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_AUTH_FAILURE; + ev.payload = "Invalid credentials or server unavailable"; + postEvent(ev); + + fprintf(stderr, "[NGMP] Login failed for user %s (HTTP %ld)\n", username.c_str(), httpCode); + fflush(stderr); + return false; +} + +bool NGMP_OnlineServicesManager::loginWithToken(const std::string& token) { + if (token.empty()) { + return false; + } + m_authToken = token; + m_isLoggedIn = true; + + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_AUTH_SUCCESS; + ev.payload = token; + postEvent(ev); + + fprintf(stderr, "[NGMP] Authenticated via saved token\n"); + fflush(stderr); + return true; +} + +void NGMP_OnlineServicesManager::logout() { + m_authToken.clear(); + m_username.clear(); + m_isLoggedIn = false; + NGMP::SaveAuthToken(""); +} diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp new file mode 100644 index 00000000000..27a44484f7f --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -0,0 +1,36 @@ +// GeneralsX @feature GeneralsOnline NGMP Init implementation +// Handles initialization and teardown of NGMP Online Services backend components. + +#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" +#include "GameNetwork/GeneralsOnline/NGMP_Helpers.h" +#include + +bool NGMP_OnlineServicesManager::init() { + if (m_initialized) { + return true; + } + + fprintf(stderr, "[NGMP] Initializing NGMP Online Services (Endpoint: %s)\n", NGMP::GetServerWSEndpoint().c_str()); + fflush(stderr); + + // Auto load token if stored locally + std::string savedToken = NGMP::LoadAuthToken(); + if (!savedToken.empty()) { + loginWithToken(savedToken); + } + + m_initialized = true; + return true; +} + +void NGMP_OnlineServicesManager::shutdown() { + if (!m_initialized) { + return; + } + + fprintf(stderr, "[NGMP] Shutting down NGMP Online Services\n"); + fflush(stderr); + + logout(); + m_initialized = false; +} diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp new file mode 100644 index 00000000000..f4de3986340 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp @@ -0,0 +1,123 @@ +// GeneralsX @feature GeneralsOnline NGMP Manager implementation +// Lifecycle management, event queue processing, and lobby REST requests. + +#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" +#include "GameNetwork/GeneralsOnline/NGMP_Helpers.h" +#include +#include +#include + +using json = nlohmann::json; + +namespace { + struct CurlResponse { + std::string text; + }; + + size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { + size_t totalSize = size * nmemb; + CurlResponse* resp = static_cast(userp); + resp->text.append(static_cast(contents), totalSize); + return totalSize; + } +} + +NGMP_OnlineServicesManager& NGMP_OnlineServicesManager::getInstance() { + static NGMP_OnlineServicesManager instance; + return instance; +} + +NGMP_OnlineServicesManager::NGMP_OnlineServicesManager() = default; +NGMP_OnlineServicesManager::~NGMP_OnlineServicesManager() { + shutdown(); +} + +void NGMP_OnlineServicesManager::postEvent(const NGMPEvent& event) { + std::lock_guard lock(m_eventMutex); + m_eventQueue.push(event); +} + +void NGMP_OnlineServicesManager::update() { + std::queue pendingEvents; + { + std::lock_guard lock(m_eventMutex); + std::swap(pendingEvents, m_eventQueue); + } + + while (!pendingEvents.empty()) { + NGMPEvent ev = pendingEvents.front(); + pendingEvents.pop(); + + switch (ev.type) { + case NGMPEvent::EVENT_AUTH_SUCCESS: + fprintf(stderr, "[NGMP-MainThread] Event: Auth Success\n"); + break; + case NGMPEvent::EVENT_AUTH_FAILURE: + fprintf(stderr, "[NGMP-MainThread] Event: Auth Failure: %s\n", ev.payload.c_str()); + break; + case NGMPEvent::EVENT_LOBBY_LIST_UPDATED: + fprintf(stderr, "[NGMP-MainThread] Event: Lobby list updated (%zu lobbies)\n", m_lobbies.size()); + break; + case NGMPEvent::EVENT_CHAT_MESSAGE_RECEIVED: + fprintf(stderr, "[NGMP-MainThread] Event: Chat msg: %s\n", ev.payload.c_str()); + break; + default: + break; + } + fflush(stderr); + } +} + +void NGMP_OnlineServicesManager::requestLobbyList() { + CURL* curl = curl_easy_init(); + if (!curl) { + return; + } + + std::string url = NGMP::GetServerRESTEndpoint() + "/lobbies"; + CurlResponse response; + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); + + CURLcode res = curl_easy_perform(curl); + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + curl_easy_cleanup(curl); + + if (res == CURLE_OK && httpCode == 200) { + try { + auto jsonList = json::parse(response.text); + m_lobbies.clear(); + if (jsonList.is_array()) { + for (const auto& item : jsonList) { + NGMPLobby lobby; + lobby.id = item.value("id", ""); + lobby.name = item.value("name", "Custom Lobby"); + lobby.mapName = item.value("mapName", "Tournament Desert"); + lobby.currentPlayers = item.value("currentPlayers", 1); + lobby.maxPlayers = item.value("maxPlayers", 8); + m_lobbies.push_back(lobby); + } + } + + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_LOBBY_LIST_UPDATED; + postEvent(ev); + } catch (const std::exception& e) { + fprintf(stderr, "[NGMP] Lobby JSON parse exception: %s\n", e.what()); + fflush(stderr); + } + } +} + +bool NGMP_OnlineServicesManager::sendChatMessage(const std::string& room, const std::string& message) { + if (!m_isLoggedIn) { + return false; + } + fprintf(stderr, "[NGMP] Sending chat message in room '%s': %s\n", room.c_str(), message.c_str()); + fflush(stderr); + return true; +} diff --git a/cmake/config-build.cmake b/cmake/config-build.cmake index aff718611db..82d99e947e6 100644 --- a/cmake/config-build.cmake +++ b/cmake/config-build.cmake @@ -136,6 +136,15 @@ if(SAGE_UPDATE_CHECK) message(STATUS "In-game update checker enabled") endif() +# GeneralsX @feature GeneralsOnline NGMP protocol option +option(SAGE_USE_NGMP "Use NGMP (GeneralsOnline) multiplayer protocol" ON) +add_feature_info(NGMPProtocol SAGE_USE_NGMP "Using NGMP multiplayer protocol (GeneralsOnline)") + +if(SAGE_USE_NGMP) + target_compile_definitions(core_config INTERFACE SAGE_USE_NGMP) + message(STATUS "NGMP (GeneralsOnline) multiplayer protocol enabled") +endif() + if(SAGE_USE_GLM) target_compile_definitions(core_config INTERFACE SAGE_USE_GLM) message(STATUS "GLM math library enabled (DirectX 8 replacement)") diff --git a/cmake/ngmp.cmake b/cmake/ngmp.cmake new file mode 100644 index 00000000000..2b1dd7c295e --- /dev/null +++ b/cmake/ngmp.cmake @@ -0,0 +1,27 @@ +# GeneralsX @feature GeneralsOnline NGMP protocol dependency setup +# Integrates nlohmann_json, libcurl, and cross-platform networking primitives. + +if(SAGE_USE_NGMP) + find_package(nlohmann_json QUIET) + if(NOT nlohmann_json_FOUND) + include(FetchContent) + FetchContent_Declare( + json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz + ) + FetchContent_MakeAvailable(json) + endif() + + find_package(CURL REQUIRED) + + if(TARGET nlohmann_json::nlohmann_json) + target_link_libraries(core_config INTERFACE nlohmann_json::nlohmann_json) + elseif(TARGET nlohmann_json) + target_link_libraries(core_config INTERFACE nlohmann_json) + endif() + + target_link_libraries(core_config INTERFACE ${CURL_LIBRARIES}) + target_include_directories(core_config INTERFACE ${CURL_INCLUDE_DIRS}) + + message(STATUS "NGMP: nlohmann_json and libcurl configured successfully") +endif() diff --git a/docs/WORKLOG/2026-08-DIARY.md b/docs/WORKLOG/2026-08-DIARY.md index 7afe8b0d01d..155f59c3771 100644 --- a/docs/WORKLOG/2026-08-DIARY.md +++ b/docs/WORKLOG/2026-08-DIARY.md @@ -6,3 +6,12 @@ - Created feature branch `feat/generals-online-ngmp` off `main`. - Added dedicated subsystem instructions in `.github/instructions/ngmp.instructions.md` with constraints on Windows OS API removal, POSIX/SDL3 abstractions, thread-safe UI queues, and default server endpoint (`ws://192.168.1.120:9001/ws`). - Registered `ngmp.instructions.md` in `AGENTS.md` instruction context loading table. + +### Phases 2-5: NGMP Protocol & Engine Integration +- Integrated `nlohmann/json` and `libcurl` via CMake (`cmake/ngmp.cmake`, `SAGE_USE_NGMP` option). +- Implemented `NGMP_Helpers` with cross-platform C++20 primitives (``, ``, ``) and storage path handling under `~/.generals_online/`. +- Implemented `NGMP_OnlineServicesManager` singleton with main-thread event queueing for thread-safe UI updates. +- Integrated auth REST calls and JWT token loading in `OnlineServices_Auth.cpp`. +- Integrated `NGMP_OnlineServicesManager` initialization and update ticks in `GameEngine.cpp`. +- Wired lobby refresh hook in `WOLLobbyMenu.cpp`. +- Validated clean compilation and linking of `GeneralsXZH` executable on macOS Vulkan target (`macos-vulkan` preset). From cb58addb86ea3348f3fb429ca148cc8295a436a2 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Mon, 3 Aug 2026 20:21:43 -0300 Subject: [PATCH 03/51] feat(ngmp): add dynamic server host and port resolution via environment and config file --- .../GeneralsOnline/NGMP_Helpers.cpp | 49 +++++++++++++++++-- cmake/ngmp.cmake | 23 +++++++++ docs/WORKLOG/2026-08-DIARY.md | 1 + 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp index edcbd951b98..1c59ce2ecb1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp @@ -9,10 +9,47 @@ #include #include +#ifndef NGMP_DEFAULT_HOST +#define NGMP_DEFAULT_HOST "192.168.1.120" +#endif + +#ifndef NGMP_DEFAULT_PORT +#define NGMP_DEFAULT_PORT "9001" +#endif + namespace NGMP { -static const char* DEFAULT_WS_ENDPOINT = "ws://192.168.1.120:9001/ws"; -static const char* DEFAULT_REST_ENDPOINT = "http://192.168.1.120:9001/api"; +static std::string GetResolvedHost() { + // 1. Check runtime environment variable NGMP_SERVER_HOST + const char* envHost = std::getenv("NGMP_SERVER_HOST"); + if (envHost && *envHost) { + return std::string(envHost); + } + + // 2. Check local file .ngmp-server-host in working directory + std::ifstream file(".ngmp-server-host"); + if (file.is_open()) { + std::string line; + if (std::getline(file, line) && !line.empty()) { + size_t first = line.find_first_not_of(" \t\r\n"); + size_t last = line.find_last_not_of(" \t\r\n"); + if (first != std::string::npos && last != std::string::npos) { + return line.substr(first, (last - first + 1)); + } + } + } + + // 3. Fallback to CMake build-time definition + return NGMP_DEFAULT_HOST; +} + +static std::string GetResolvedPort() { + const char* envPort = std::getenv("NGMP_SERVER_PORT"); + if (envPort && *envPort) { + return std::string(envPort); + } + return NGMP_DEFAULT_PORT; +} uint32_t GetTicks() { auto now = std::chrono::steady_clock::now(); @@ -62,11 +99,15 @@ std::string LoadAuthToken() { } std::string GetServerWSEndpoint() { - return DEFAULT_WS_ENDPOINT; + std::string host = GetResolvedHost(); + std::string port = GetResolvedPort(); + return "ws://" + host + ":" + port + "/ws"; } std::string GetServerRESTEndpoint() { - return DEFAULT_REST_ENDPOINT; + std::string host = GetResolvedHost(); + std::string port = GetResolvedPort(); + return "http://" + host + ":" + port + "/api"; } } // namespace NGMP diff --git a/cmake/ngmp.cmake b/cmake/ngmp.cmake index 2b1dd7c295e..8375959aa97 100644 --- a/cmake/ngmp.cmake +++ b/cmake/ngmp.cmake @@ -23,5 +23,28 @@ if(SAGE_USE_NGMP) target_link_libraries(core_config INTERFACE ${CURL_LIBRARIES}) target_include_directories(core_config INTERFACE ${CURL_INCLUDE_DIRS}) + # NGMP Server Host & Port configuration (File > Environment > Default) + if(EXISTS "${CMAKE_SOURCE_DIR}/.ngmp-server-host") + file(READ "${CMAKE_SOURCE_DIR}/.ngmp-server-host" NGMP_SERVER_HOST) + string(STRIP "${NGMP_SERVER_HOST}" NGMP_SERVER_HOST) + set(NGMP_SERVER_HOST "${NGMP_SERVER_HOST}" CACHE STRING "NGMP Server Host" FORCE) + elseif(DEFINED ENV{NGMP_SERVER_HOST}) + set(NGMP_SERVER_HOST "$ENV{NGMP_SERVER_HOST}" CACHE STRING "NGMP Server Host" FORCE) + else() + set(NGMP_SERVER_HOST "192.168.1.120" CACHE STRING "NGMP Server Host") + endif() + + if(DEFINED ENV{NGMP_SERVER_PORT}) + set(NGMP_SERVER_PORT "$ENV{NGMP_SERVER_PORT}" CACHE STRING "NGMP Server Port" FORCE) + else() + set(NGMP_SERVER_PORT "9001" CACHE STRING "NGMP Server Port") + endif() + + target_compile_definitions(core_config INTERFACE + NGMP_DEFAULT_HOST="${NGMP_SERVER_HOST}" + NGMP_DEFAULT_PORT="${NGMP_SERVER_PORT}" + ) + + message(STATUS "NGMP: Server target configured to ${NGMP_SERVER_HOST}:${NGMP_SERVER_PORT}") message(STATUS "NGMP: nlohmann_json and libcurl configured successfully") endif() diff --git a/docs/WORKLOG/2026-08-DIARY.md b/docs/WORKLOG/2026-08-DIARY.md index 155f59c3771..128a98c414f 100644 --- a/docs/WORKLOG/2026-08-DIARY.md +++ b/docs/WORKLOG/2026-08-DIARY.md @@ -14,4 +14,5 @@ - Integrated auth REST calls and JWT token loading in `OnlineServices_Auth.cpp`. - Integrated `NGMP_OnlineServicesManager` initialization and update ticks in `GameEngine.cpp`. - Wired lobby refresh hook in `WOLLobbyMenu.cpp`. +- Added dynamic multi-layered NGMP server host/port resolution in `cmake/ngmp.cmake` and `NGMP_Helpers.cpp` prioritizing (1) runtime environment variables (`NGMP_SERVER_HOST`/`NGMP_SERVER_PORT`), (2) local `.ngmp-server-host` file, and (3) CMake build-time defaults (`-DNGMP_SERVER_HOST`). - Validated clean compilation and linking of `GeneralsXZH` executable on macOS Vulkan target (`macos-vulkan` preset). From 186f8da1e69c654dc7f5bca48ab2a22578723645 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Mon, 3 Aug 2026 20:57:47 -0300 Subject: [PATCH 04/51] feat(ngmp): set exact default ports 9001 for HTTP/WS and 9000 for HTTPS/WSS --- .../GameNetwork/GeneralsOnline/NGMP_Helpers.h | 9 +++ .../GeneralsOnline/NGMP_Helpers.cpp | 75 +++++++++++++++---- cmake/ngmp.cmake | 4 +- 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h index 6ca1b7c2926..74da207a3f8 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h @@ -24,6 +24,15 @@ bool SaveAuthToken(const std::string& token); // Loads authentication token from local user storage std::string LoadAuthToken(); +// Returns true if SSL (HTTPS/WSS) is enabled +bool IsSSLEnabled(); + +// Returns default insecure port (9001) +std::string GetServerHTTPPort(); + +// Returns default secure SSL port (9000) +std::string GetServerSSLPort(); + // Returns default server WS endpoint URL std::string GetServerWSEndpoint(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp index 1c59ce2ecb1..3624b4b71e8 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp @@ -8,20 +8,71 @@ #include #include #include +#include #ifndef NGMP_DEFAULT_HOST -#define NGMP_DEFAULT_HOST "192.168.1.120" +#define NGMP_DEFAULT_HOST "localhost" #endif #ifndef NGMP_DEFAULT_PORT #define NGMP_DEFAULT_PORT "9001" #endif +#ifndef NGMP_DEFAULT_SSL_PORT +#define NGMP_DEFAULT_SSL_PORT "9000" +#endif + namespace NGMP { +bool IsSSLEnabled() { + // 1. Check runtime environment variable NGMP_USE_SSL / NGMP_SSL + const char* envSSL = std::getenv("NGMP_USE_SSL"); + if (!envSSL) { + envSSL = std::getenv("NGMP_SSL"); + } + if (envSSL && *envSSL) { + std::string val = envSSL; + std::transform(val.begin(), val.end(), val.begin(), ::tolower); + if (val == "1" || val == "true" || val == "yes" || val == "on") { + return true; + } + } + +#if defined(NGMP_USE_SSL) && NGMP_USE_SSL + return true; +#else + return false; +#endif +} + +std::string GetServerHTTPPort() { + const char* envPort = std::getenv("NGMP_HTTP_PORT"); + if (!envPort || !*envPort) { + envPort = std::getenv("NGMP_SERVER_PORT"); + } + if (envPort && *envPort) { + return std::string(envPort); + } + return NGMP_DEFAULT_PORT; +} + +std::string GetServerSSLPort() { + const char* envPort = std::getenv("NGMP_SSL_PORT"); + if (!envPort || !*envPort) { + envPort = std::getenv("NGMP_HTTPS_PORT"); + } + if (envPort && *envPort) { + return std::string(envPort); + } + return NGMP_DEFAULT_SSL_PORT; +} + static std::string GetResolvedHost() { - // 1. Check runtime environment variable NGMP_SERVER_HOST + // 1. Check runtime environment variables (NGMP_SERVER_HOST or NGMP_DEFAULT_HOST) const char* envHost = std::getenv("NGMP_SERVER_HOST"); + if (!envHost || !*envHost) { + envHost = std::getenv("NGMP_DEFAULT_HOST"); + } if (envHost && *envHost) { return std::string(envHost); } @@ -43,14 +94,6 @@ static std::string GetResolvedHost() { return NGMP_DEFAULT_HOST; } -static std::string GetResolvedPort() { - const char* envPort = std::getenv("NGMP_SERVER_PORT"); - if (envPort && *envPort) { - return std::string(envPort); - } - return NGMP_DEFAULT_PORT; -} - uint32_t GetTicks() { auto now = std::chrono::steady_clock::now(); return static_cast( @@ -100,14 +143,18 @@ std::string LoadAuthToken() { std::string GetServerWSEndpoint() { std::string host = GetResolvedHost(); - std::string port = GetResolvedPort(); - return "ws://" + host + ":" + port + "/ws"; + if (IsSSLEnabled()) { + return "wss://" + host + ":" + GetServerSSLPort() + "/ws"; + } + return "ws://" + host + ":" + GetServerHTTPPort() + "/ws"; } std::string GetServerRESTEndpoint() { std::string host = GetResolvedHost(); - std::string port = GetResolvedPort(); - return "http://" + host + ":" + port + "/api"; + if (IsSSLEnabled()) { + return "https://" + host + ":" + GetServerSSLPort() + "/api"; + } + return "http://" + host + ":" + GetServerHTTPPort() + "/api"; } } // namespace NGMP diff --git a/cmake/ngmp.cmake b/cmake/ngmp.cmake index 8375959aa97..89d5c28438f 100644 --- a/cmake/ngmp.cmake +++ b/cmake/ngmp.cmake @@ -30,8 +30,10 @@ if(SAGE_USE_NGMP) set(NGMP_SERVER_HOST "${NGMP_SERVER_HOST}" CACHE STRING "NGMP Server Host" FORCE) elseif(DEFINED ENV{NGMP_SERVER_HOST}) set(NGMP_SERVER_HOST "$ENV{NGMP_SERVER_HOST}" CACHE STRING "NGMP Server Host" FORCE) + elseif(DEFINED ENV{NGMP_DEFAULT_HOST}) + set(NGMP_SERVER_HOST "$ENV{NGMP_DEFAULT_HOST}" CACHE STRING "NGMP Server Host" FORCE) else() - set(NGMP_SERVER_HOST "192.168.1.120" CACHE STRING "NGMP Server Host") + set(NGMP_SERVER_HOST "localhost" CACHE STRING "NGMP Server Host") endif() if(DEFINED ENV{NGMP_SERVER_PORT}) From a991c7f4428b7a35cc21f0476967161d248f5e74 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Mon, 3 Aug 2026 23:18:46 -0300 Subject: [PATCH 05/51] feat(ngmp): hardcode server host and ssl configuration at build-time --- .../GeneralsOnline/NGMP_Helpers.cpp | 74 +------------------ cmake/ngmp.cmake | 19 ++++- docs/WORKLOG/2026-08-DIARY.md | 2 +- 3 files changed, 23 insertions(+), 72 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp index 3624b4b71e8..3784218f10e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp @@ -18,26 +18,9 @@ #define NGMP_DEFAULT_PORT "9001" #endif -#ifndef NGMP_DEFAULT_SSL_PORT -#define NGMP_DEFAULT_SSL_PORT "9000" -#endif - namespace NGMP { bool IsSSLEnabled() { - // 1. Check runtime environment variable NGMP_USE_SSL / NGMP_SSL - const char* envSSL = std::getenv("NGMP_USE_SSL"); - if (!envSSL) { - envSSL = std::getenv("NGMP_SSL"); - } - if (envSSL && *envSSL) { - std::string val = envSSL; - std::transform(val.begin(), val.end(), val.begin(), ::tolower); - if (val == "1" || val == "true" || val == "yes" || val == "on") { - return true; - } - } - #if defined(NGMP_USE_SSL) && NGMP_USE_SSL return true; #else @@ -45,54 +28,7 @@ bool IsSSLEnabled() { #endif } -std::string GetServerHTTPPort() { - const char* envPort = std::getenv("NGMP_HTTP_PORT"); - if (!envPort || !*envPort) { - envPort = std::getenv("NGMP_SERVER_PORT"); - } - if (envPort && *envPort) { - return std::string(envPort); - } - return NGMP_DEFAULT_PORT; -} - -std::string GetServerSSLPort() { - const char* envPort = std::getenv("NGMP_SSL_PORT"); - if (!envPort || !*envPort) { - envPort = std::getenv("NGMP_HTTPS_PORT"); - } - if (envPort && *envPort) { - return std::string(envPort); - } - return NGMP_DEFAULT_SSL_PORT; -} -static std::string GetResolvedHost() { - // 1. Check runtime environment variables (NGMP_SERVER_HOST or NGMP_DEFAULT_HOST) - const char* envHost = std::getenv("NGMP_SERVER_HOST"); - if (!envHost || !*envHost) { - envHost = std::getenv("NGMP_DEFAULT_HOST"); - } - if (envHost && *envHost) { - return std::string(envHost); - } - - // 2. Check local file .ngmp-server-host in working directory - std::ifstream file(".ngmp-server-host"); - if (file.is_open()) { - std::string line; - if (std::getline(file, line) && !line.empty()) { - size_t first = line.find_first_not_of(" \t\r\n"); - size_t last = line.find_last_not_of(" \t\r\n"); - if (first != std::string::npos && last != std::string::npos) { - return line.substr(first, (last - first + 1)); - } - } - } - - // 3. Fallback to CMake build-time definition - return NGMP_DEFAULT_HOST; -} uint32_t GetTicks() { auto now = std::chrono::steady_clock::now(); @@ -142,19 +78,17 @@ std::string LoadAuthToken() { } std::string GetServerWSEndpoint() { - std::string host = GetResolvedHost(); if (IsSSLEnabled()) { - return "wss://" + host + ":" + GetServerSSLPort() + "/ws"; + return "wss://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/ws"; } - return "ws://" + host + ":" + GetServerHTTPPort() + "/ws"; + return "ws://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/ws"; } std::string GetServerRESTEndpoint() { - std::string host = GetResolvedHost(); if (IsSSLEnabled()) { - return "https://" + host + ":" + GetServerSSLPort() + "/api"; + return "https://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/api"; } - return "http://" + host + ":" + GetServerHTTPPort() + "/api"; + return "http://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/api"; } } // namespace NGMP diff --git a/cmake/ngmp.cmake b/cmake/ngmp.cmake index 89d5c28438f..9bce11bdeb6 100644 --- a/cmake/ngmp.cmake +++ b/cmake/ngmp.cmake @@ -42,11 +42,28 @@ if(SAGE_USE_NGMP) set(NGMP_SERVER_PORT "9001" CACHE STRING "NGMP Server Port") endif() + if(DEFINED ENV{NGMP_USE_SSL}) + if("$ENV{NGMP_USE_SSL}" MATCHES "^(1|ON|YES|TRUE|Y|on|yes|true|y)$") + option(NGMP_USE_SSL "Enable SSL for NGMP protocol" ON) + else() + option(NGMP_USE_SSL "Enable SSL for NGMP protocol" OFF) + endif() + else() + option(NGMP_USE_SSL "Enable SSL for NGMP protocol" OFF) + endif() + target_compile_definitions(core_config INTERFACE NGMP_DEFAULT_HOST="${NGMP_SERVER_HOST}" NGMP_DEFAULT_PORT="${NGMP_SERVER_PORT}" ) - message(STATUS "NGMP: Server target configured to ${NGMP_SERVER_HOST}:${NGMP_SERVER_PORT}") + if(NGMP_USE_SSL) + target_compile_definitions(core_config INTERFACE NGMP_USE_SSL=1) + message(STATUS "NGMP: Server target configured to wss://${NGMP_SERVER_HOST}:${NGMP_SERVER_PORT}") + else() + target_compile_definitions(core_config INTERFACE NGMP_USE_SSL=0) + message(STATUS "NGMP: Server target configured to ws://${NGMP_SERVER_HOST}:${NGMP_SERVER_PORT}") + endif() + message(STATUS "NGMP: nlohmann_json and libcurl configured successfully") endif() diff --git a/docs/WORKLOG/2026-08-DIARY.md b/docs/WORKLOG/2026-08-DIARY.md index 128a98c414f..326ce08d1b3 100644 --- a/docs/WORKLOG/2026-08-DIARY.md +++ b/docs/WORKLOG/2026-08-DIARY.md @@ -14,5 +14,5 @@ - Integrated auth REST calls and JWT token loading in `OnlineServices_Auth.cpp`. - Integrated `NGMP_OnlineServicesManager` initialization and update ticks in `GameEngine.cpp`. - Wired lobby refresh hook in `WOLLobbyMenu.cpp`. -- Added dynamic multi-layered NGMP server host/port resolution in `cmake/ngmp.cmake` and `NGMP_Helpers.cpp` prioritizing (1) runtime environment variables (`NGMP_SERVER_HOST`/`NGMP_SERVER_PORT`), (2) local `.ngmp-server-host` file, and (3) CMake build-time defaults (`-DNGMP_SERVER_HOST`). +- Hardcoded NGMP server host/port and SSL configuration at CMake compile-time (`cmake/ngmp.cmake` and `NGMP_Helpers.cpp`). Stripped all runtime `std::getenv` and local file reads from C++ client to conceal connection details in binary. Added `NGMP_USE_SSL` CMake option with CMake-time `ENV{NGMP_USE_SSL}` fallback. - Validated clean compilation and linking of `GeneralsXZH` executable on macOS Vulkan target (`macos-vulkan` preset). From b460fc5dc00b86f1de1fba02c6a0970bcb091066 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Tue, 4 Aug 2026 18:06:24 -0300 Subject: [PATCH 06/51] feat(ngmp): async http requests, websocket chat, and lobby ui hook --- GeneralsMD/Code/GameEngine/CMakeLists.txt | 3 + .../GeneralsOnline/NGMPChatSession.h | 49 ++++++ .../GameNetwork/GeneralsOnline/NGMP_Helpers.h | 6 - .../GeneralsOnline/OnlineServices_Manager.h | 25 ++- .../GeneralsOnline/ngmp_curl_utils.h | 23 +++ .../GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp | 9 + .../GeneralsOnline/OnlineServices_Auth.cpp | 157 +++++++++--------- .../GeneralsOnline/OnlineServices_Chat.cpp | 137 +++++++++++++++ .../GeneralsOnline/OnlineServices_Init.cpp | 34 +++- .../GeneralsOnline/OnlineServices_Manager.cpp | 124 ++++++++------ docs/WORKLOG/2026-08-DIARY.md | 13 ++ 11 files changed, 445 insertions(+), 135 deletions(-) create mode 100644 GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPChatSession.h create mode 100644 GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/ngmp_curl_utils.h create mode 100644 GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Chat.cpp diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index b25c58b2452..73106eec4ce 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -1113,9 +1113,12 @@ set(GAMEENGINE_SRC Source/GameNetwork/GUIUtil.cpp Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h + Include/GameNetwork/GeneralsOnline/NGMPChatSession.h + Include/GameNetwork/GeneralsOnline/ngmp_curl_utils.h Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp + Source/GameNetwork/GeneralsOnline/OnlineServices_Chat.cpp Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp # Source/GameNetwork/IPEnumeration.cpp # Source/GameNetwork/LANAPI.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPChatSession.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPChatSession.h new file mode 100644 index 00000000000..22e3e2decd0 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPChatSession.h @@ -0,0 +1,49 @@ +// GeneralsX @feature GeneralsOnline NGMP Chat WebSocket session header +// Manages a persistent WebSocket connection to the NGMP backend chat server. + +#ifndef NGMP_CHAT_SESSION_H +#define NGMP_CHAT_SESSION_H + +#include +#include +#include +#include +#include + +namespace NGMP { + +using ChatMessageCallback = std::function; + +class NGMPChatSession { +public: + NGMPChatSession() = default; + ~NGMPChatSession(); + + // Connect to the WebSocket endpoint (blocking until connected or failed) + bool connect(const std::string& wsUrl, const std::string& authToken); + + // Disconnect from the WebSocket and stop the receiver thread + void disconnect(); + + // Returns true if currently connected + bool isConnected() const { return m_running.load(); } + + // Send a chat message in the given room + bool sendMessage(const std::string& room, const std::string& message); + + // Set the callback invoked on the receiver thread when a message arrives + // NOTE: callback must post to the NGMP event queue, not touch UI directly + void setMessageCallback(ChatMessageCallback cb) { m_messageCallback = std::move(cb); } + +private: + void receiveLoop(); + + CURL* m_curl = nullptr; + std::thread m_recvThread; + std::atomic m_running = false; + ChatMessageCallback m_messageCallback; +}; + +} // namespace NGMP + +#endif // NGMP_CHAT_SESSION_H diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h index 74da207a3f8..f526380be21 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h @@ -27,12 +27,6 @@ std::string LoadAuthToken(); // Returns true if SSL (HTTPS/WSS) is enabled bool IsSSLEnabled(); -// Returns default insecure port (9001) -std::string GetServerHTTPPort(); - -// Returns default secure SSL port (9000) -std::string GetServerSSLPort(); - // Returns default server WS endpoint URL std::string GetServerWSEndpoint(); diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h index 0163b11774b..9f24b6a8b31 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h @@ -4,11 +4,14 @@ #ifndef ONLINE_SERVICES_MANAGER_H #define ONLINE_SERVICES_MANAGER_H +#include "GameNetwork/GeneralsOnline/NGMPChatSession.h" #include #include #include #include #include +#include +#include struct NGMPEvent { enum Type { @@ -17,6 +20,8 @@ struct NGMPEvent { EVENT_AUTH_FAILURE, EVENT_LOBBY_LIST_UPDATED, EVENT_CHAT_MESSAGE_RECEIVED, + EVENT_CHAT_CONNECTED, + EVENT_CHAT_DISCONNECTED, EVENT_DISCONNECTED }; @@ -40,11 +45,14 @@ class NGMP_OnlineServicesManager { void update(); // Main thread UI tick dispatch void shutdown(); - bool login(const std::string& username, const std::string& password); + // Async login — result delivered via EVENT_AUTH_SUCCESS / EVENT_AUTH_FAILURE + void loginAsync(const std::string& username, const std::string& password); bool loginWithToken(const std::string& token); void logout(); - void requestLobbyList(); + // Async lobby fetch — result delivered via EVENT_LOBBY_LIST_UPDATED + void requestLobbyListAsync(); + bool sendChatMessage(const std::string& room, const std::string& message); bool isLoggedIn() const { return m_isLoggedIn; } @@ -52,7 +60,7 @@ class NGMP_OnlineServicesManager { std::string getUsername() const { return m_username; } const std::vector& getLobbies() const { return m_lobbies; } - // Internal thread-safe event poster + // Internal thread-safe event poster (called from worker threads) void postEvent(const NGMPEvent& event); private: @@ -68,6 +76,17 @@ class NGMP_OnlineServicesManager { std::string m_authToken; std::vector m_lobbies; + // Async login state + std::atomic m_loginInFlight = false; + std::thread m_loginThread; + + // Async lobby request state + std::atomic m_lobbyRequestInFlight = false; + std::thread m_lobbyThread; + + // Chat WebSocket session + std::unique_ptr m_chatSession; + mutable std::mutex m_eventMutex; std::queue m_eventQueue; }; diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/ngmp_curl_utils.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/ngmp_curl_utils.h new file mode 100644 index 00000000000..d66aef7a90e --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/ngmp_curl_utils.h @@ -0,0 +1,23 @@ +// GeneralsX @feature GeneralsOnline Internal libcurl write callback utilities +// Shared by OnlineServices_Auth.cpp and OnlineServices_Manager.cpp to avoid code duplication. + +#pragma once +#include +#include + +namespace NGMP { +namespace Internal { + +struct CurlResponse { + std::string text; +}; + +inline size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { + size_t totalSize = size * nmemb; + CurlResponse* resp = static_cast(userp); + resp->text.append(static_cast(contents), totalSize); + return totalSize; +} + +} // namespace Internal +} // namespace NGMP diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp index 0c321eef441..69062298cff 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp @@ -70,6 +70,10 @@ #include "GameNetwork/GameSpy/LobbyUtils.h" #include "GameNetwork/RankPointValue.h" +#if defined(SAGE_USE_NGMP) +#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" +#endif + void refreshGameList( Bool forceRefresh = FALSE ); void refreshPlayerList( Bool forceRefresh = FALSE ); @@ -723,6 +727,11 @@ void WOLLobbyMenuInit( WindowLayout *layout, void *userData ) win->winHide(TRUE); DontShowMainMenu = TRUE; +#if defined(SAGE_USE_NGMP) + // GeneralsX @feature GeneralsOnline Kick off async lobby list refresh on menu init + NGMP_OnlineServicesManager::getInstance().requestLobbyListAsync(); +#endif + } //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp index e376ae2fe54..73a5cfb38bb 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp @@ -3,95 +3,104 @@ #include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" #include "GameNetwork/GeneralsOnline/NGMP_Helpers.h" +#include "GameNetwork/GeneralsOnline/ngmp_curl_utils.h" #include -#include +#include #include using json = nlohmann::json; -namespace { - struct CurlResponse { - std::string text; - }; - - size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { - size_t totalSize = size * nmemb; - CurlResponse* resp = static_cast(userp); - resp->text.append(static_cast(contents), totalSize); - return totalSize; +void NGMP_OnlineServicesManager::loginAsync(const std::string& username, const std::string& password) { + if (m_loginInFlight.exchange(true)) { + fprintf(stderr, "[NGMP] Login already in flight, ignoring duplicate request\n"); + fflush(stderr); + return; } -} -bool NGMP_OnlineServicesManager::login(const std::string& username, const std::string& password) { - fprintf(stderr, "[NGMP] Attempting login for user: %s\n", username.c_str()); - fflush(stderr); + if (m_loginThread.joinable()) { + m_loginThread.join(); + } - CURL* curl = curl_easy_init(); - if (!curl) { - fprintf(stderr, "[NGMP] Failed to initialize libcurl for auth\n"); + m_loginThread = std::thread([this, username, password]() { + fprintf(stderr, "[NGMP] Attempting login for user: %s\n", username.c_str()); fflush(stderr); - return false; - } - std::string url = NGMP::GetServerRESTEndpoint() + "/auth/login"; - json requestJson = { - {"username", username}, - {"password", password} - }; - std::string requestBody = requestJson.dump(); - - CurlResponse response; - struct curl_slist* headers = nullptr; - headers = curl_slist_append(headers, "Content-Type: application/json"); - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, requestBody.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); - - CURLcode res = curl_easy_perform(curl); - long httpCode = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); - - curl_slist_free_all(headers); - curl_easy_cleanup(curl); - - if (res == CURLE_OK && (httpCode == 200 || httpCode == 201)) { - try { - auto responseJson = json::parse(response.text); - if (responseJson.contains("token")) { - m_authToken = responseJson["token"].get(); - m_username = username; - m_isLoggedIn = true; - - NGMP::SaveAuthToken(m_authToken); - - NGMPEvent ev; - ev.type = NGMPEvent::EVENT_AUTH_SUCCESS; - ev.payload = m_authToken; - postEvent(ev); - - fprintf(stderr, "[NGMP] Login successful for user: %s\n", username.c_str()); + CURL* curl = curl_easy_init(); + if (!curl) { + fprintf(stderr, "[NGMP] Failed to initialize libcurl for auth\n"); + fflush(stderr); + m_loginInFlight = false; + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_AUTH_FAILURE; + ev.payload = "Failed to initialize HTTP client"; + postEvent(ev); + return; + } + + std::string url = NGMP::GetServerRESTEndpoint() + "/auth/login"; + json requestJson = { + {"username", username}, + {"password", password} + }; + std::string requestBody = requestJson.dump(); + + NGMP::Internal::CurlResponse response; + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, requestBody.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NGMP::Internal::WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + + CURLcode res = curl_easy_perform(curl); + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (res == CURLE_OK && (httpCode == 200 || httpCode == 201)) { + try { + auto responseJson = json::parse(response.text); + if (responseJson.contains("token")) { + std::string token = responseJson["token"].get(); + + // Update state (write from worker thread, read-only from main thread until event arrives) + m_authToken = token; + m_username = username; + m_isLoggedIn = true; + + NGMP::SaveAuthToken(token); + + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_AUTH_SUCCESS; + ev.payload = token; + postEvent(ev); + + fprintf(stderr, "[NGMP] Login successful for user: %s\n", username.c_str()); + fflush(stderr); + m_loginInFlight = false; + return; + } + } catch (const std::exception& e) { + fprintf(stderr, "[NGMP] JSON parse exception during auth: %s\n", e.what()); fflush(stderr); - return true; } - } catch (const std::exception& e) { - fprintf(stderr, "[NGMP] JSON parse exception during auth: %s\n", e.what()); - fflush(stderr); } - } - // Auth failed - NGMPEvent ev; - ev.type = NGMPEvent::EVENT_AUTH_FAILURE; - ev.payload = "Invalid credentials or server unavailable"; - postEvent(ev); + // Auth failed + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_AUTH_FAILURE; + ev.payload = "Invalid credentials or server unavailable"; + postEvent(ev); - fprintf(stderr, "[NGMP] Login failed for user %s (HTTP %ld)\n", username.c_str(), httpCode); - fflush(stderr); - return false; + fprintf(stderr, "[NGMP] Login failed for user %s (HTTP %ld)\n", username.c_str(), httpCode); + fflush(stderr); + m_loginInFlight = false; + }); } bool NGMP_OnlineServicesManager::loginWithToken(const std::string& token) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Chat.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Chat.cpp new file mode 100644 index 00000000000..4ac9fc9af11 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Chat.cpp @@ -0,0 +1,137 @@ +// GeneralsX @feature GeneralsOnline NGMP Chat WebSocket implementation +// Persistent WS connection using libcurl WebSocket (>= 7.86.0) for bidirectional chat. + +#include "GameNetwork/GeneralsOnline/NGMPChatSession.h" +#include +#include +#include + +using json = nlohmann::json; + +namespace NGMP { + +NGMPChatSession::~NGMPChatSession() { + disconnect(); +} + +bool NGMPChatSession::connect(const std::string& wsUrl, const std::string& authToken) { + if (m_running.load()) { + return true; // Already connected + } + + m_curl = curl_easy_init(); + if (!m_curl) { + fprintf(stderr, "[NGMP-Chat] Failed to initialize libcurl for WebSocket\n"); + fflush(stderr); + return false; + } + + struct curl_slist* headers = nullptr; + if (!authToken.empty()) { + std::string authHeader = "Authorization: Bearer " + authToken; + headers = curl_slist_append(headers, authHeader.c_str()); + } + + curl_easy_setopt(m_curl, CURLOPT_URL, wsUrl.c_str()); + curl_easy_setopt(m_curl, CURLOPT_CONNECT_ONLY, 2L); // WebSocket mode + if (headers) { + curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, headers); + } + + CURLcode res = curl_easy_perform(m_curl); + if (headers) { + curl_slist_free_all(headers); + } + + if (res != CURLE_OK) { + fprintf(stderr, "[NGMP-Chat] WebSocket connect failed: %s\n", curl_easy_strerror(res)); + fflush(stderr); + curl_easy_cleanup(m_curl); + m_curl = nullptr; + return false; + } + + fprintf(stderr, "[NGMP-Chat] WebSocket connected to %s\n", wsUrl.c_str()); + fflush(stderr); + + m_running = true; + m_recvThread = std::thread(&NGMPChatSession::receiveLoop, this); + return true; +} + +void NGMPChatSession::disconnect() { + m_running = false; + if (m_recvThread.joinable()) { + m_recvThread.join(); + } + if (m_curl) { + curl_easy_cleanup(m_curl); + m_curl = nullptr; + } + fprintf(stderr, "[NGMP-Chat] WebSocket disconnected\n"); + fflush(stderr); +} + +bool NGMPChatSession::sendMessage(const std::string& room, const std::string& message) { + if (!m_running.load() || !m_curl) { + return false; + } + + json payload = { + {"type", "chat"}, + {"room", room}, + {"message", message} + }; + std::string frame = payload.dump(); + + size_t sent = 0; + CURLcode res = curl_ws_send(m_curl, frame.c_str(), frame.size(), &sent, 0, CURLWS_TEXT); + if (res != CURLE_OK) { + fprintf(stderr, "[NGMP-Chat] Failed to send WS message: %s\n", curl_easy_strerror(res)); + fflush(stderr); + return false; + } + return true; +} + +void NGMPChatSession::receiveLoop() { + char buffer[4096]; + const struct curl_ws_frame* meta = nullptr; + + while (m_running.load()) { + size_t received = 0; + CURLcode res = curl_ws_recv(m_curl, buffer, sizeof(buffer) - 1, &received, &meta); + + if (res == CURLE_AGAIN) { + // No data ready — yield briefly to avoid busy-spinning + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + continue; + } + + if (res != CURLE_OK) { + fprintf(stderr, "[NGMP-Chat] WS recv error: %s\n", curl_easy_strerror(res)); + fflush(stderr); + m_running = false; + break; + } + + if (received > 0) { + buffer[received] = '\0'; + try { + auto msg = json::parse(buffer); + std::string type = msg.value("type", ""); + if (type == "chat" && m_messageCallback) { + std::string room = msg.value("room", ""); + std::string sender = msg.value("sender", ""); + std::string content = msg.value("message", ""); + m_messageCallback(room, sender, content); + } + } catch (const std::exception& e) { + fprintf(stderr, "[NGMP-Chat] JSON parse error in WS frame: %s\n", e.what()); + fflush(stderr); + } + } + } +} + +} // namespace NGMP diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp index 27a44484f7f..4f73dbfa6d3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -16,7 +16,22 @@ bool NGMP_OnlineServicesManager::init() { // Auto load token if stored locally std::string savedToken = NGMP::LoadAuthToken(); if (!savedToken.empty()) { - loginWithToken(savedToken); + if (loginWithToken(savedToken)) { + // Connect chat WebSocket using the restored session token + m_chatSession = std::make_unique(); + m_chatSession->setMessageCallback([this](const std::string& room, const std::string& sender, const std::string& msg) { + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_CHAT_MESSAGE_RECEIVED; + ev.payload = "[" + room + "] " + sender + ": " + msg; + postEvent(ev); + }); + bool connected = m_chatSession->connect(NGMP::GetServerWSEndpoint(), savedToken); + if (connected) { + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_CHAT_CONNECTED; + postEvent(ev); + } + } } m_initialized = true; @@ -31,6 +46,23 @@ void NGMP_OnlineServicesManager::shutdown() { fprintf(stderr, "[NGMP] Shutting down NGMP Online Services\n"); fflush(stderr); + // Disconnect chat first + if (m_chatSession) { + m_chatSession->disconnect(); + m_chatSession.reset(); + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_CHAT_DISCONNECTED; + postEvent(ev); + } + + // Join any in-flight threads before state is destroyed + if (m_loginThread.joinable()) { + m_loginThread.join(); + } + if (m_lobbyThread.joinable()) { + m_lobbyThread.join(); + } + logout(); m_initialized = false; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp index f4de3986340..0e27495ffbc 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp @@ -3,25 +3,14 @@ #include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" #include "GameNetwork/GeneralsOnline/NGMP_Helpers.h" +#include "GameNetwork/GeneralsOnline/ngmp_curl_utils.h" #include +#include #include #include using json = nlohmann::json; -namespace { - struct CurlResponse { - std::string text; - }; - - size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { - size_t totalSize = size * nmemb; - CurlResponse* resp = static_cast(userp); - resp->text.append(static_cast(contents), totalSize); - return totalSize; - } -} - NGMP_OnlineServicesManager& NGMP_OnlineServicesManager::getInstance() { static NGMP_OnlineServicesManager instance; return instance; @@ -61,6 +50,12 @@ void NGMP_OnlineServicesManager::update() { case NGMPEvent::EVENT_CHAT_MESSAGE_RECEIVED: fprintf(stderr, "[NGMP-MainThread] Event: Chat msg: %s\n", ev.payload.c_str()); break; + case NGMPEvent::EVENT_CHAT_CONNECTED: + fprintf(stderr, "[NGMP-MainThread] Event: Chat connected\n"); + break; + case NGMPEvent::EVENT_CHAT_DISCONNECTED: + fprintf(stderr, "[NGMP-MainThread] Event: Chat disconnected\n"); + break; default: break; } @@ -68,56 +63,83 @@ void NGMP_OnlineServicesManager::update() { } } -void NGMP_OnlineServicesManager::requestLobbyList() { - CURL* curl = curl_easy_init(); - if (!curl) { +void NGMP_OnlineServicesManager::requestLobbyListAsync() { + if (m_lobbyRequestInFlight.exchange(true)) { + fprintf(stderr, "[NGMP] Lobby request already in flight, ignoring duplicate\n"); + fflush(stderr); return; } - std::string url = NGMP::GetServerRESTEndpoint() + "/lobbies"; - CurlResponse response; - - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); - - CURLcode res = curl_easy_perform(curl); - long httpCode = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); - curl_easy_cleanup(curl); - - if (res == CURLE_OK && httpCode == 200) { - try { - auto jsonList = json::parse(response.text); - m_lobbies.clear(); - if (jsonList.is_array()) { - for (const auto& item : jsonList) { - NGMPLobby lobby; - lobby.id = item.value("id", ""); - lobby.name = item.value("name", "Custom Lobby"); - lobby.mapName = item.value("mapName", "Tournament Desert"); - lobby.currentPlayers = item.value("currentPlayers", 1); - lobby.maxPlayers = item.value("maxPlayers", 8); - m_lobbies.push_back(lobby); + if (m_lobbyThread.joinable()) { + m_lobbyThread.join(); + } + + m_lobbyThread = std::thread([this]() { + CURL* curl = curl_easy_init(); + if (!curl) { + m_lobbyRequestInFlight = false; + return; + } + + std::string url = NGMP::GetServerRESTEndpoint() + "/lobbies"; + NGMP::Internal::CurlResponse response; + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NGMP::Internal::WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); + + CURLcode res = curl_easy_perform(curl); + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + curl_easy_cleanup(curl); + + if (res == CURLE_OK && httpCode == 200) { + try { + auto jsonList = json::parse(response.text); + std::vector lobbies; + if (jsonList.is_array()) { + for (const auto& item : jsonList) { + NGMPLobby lobby; + lobby.id = item.value("id", ""); + lobby.name = item.value("name", "Custom Lobby"); + lobby.mapName = item.value("mapName", "Tournament Desert"); + lobby.currentPlayers = item.value("currentPlayers", 1); + lobby.maxPlayers = item.value("maxPlayers", 8); + lobbies.push_back(lobby); + } + } + + // Swap into member under the event mutex for safe handoff + { + std::lock_guard lock(m_eventMutex); + m_lobbies = std::move(lobbies); } - } - NGMPEvent ev; - ev.type = NGMPEvent::EVENT_LOBBY_LIST_UPDATED; - postEvent(ev); - } catch (const std::exception& e) { - fprintf(stderr, "[NGMP] Lobby JSON parse exception: %s\n", e.what()); + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_LOBBY_LIST_UPDATED; + postEvent(ev); + } catch (const std::exception& e) { + fprintf(stderr, "[NGMP] Lobby JSON parse exception: %s\n", e.what()); + fflush(stderr); + } + } else { + fprintf(stderr, "[NGMP] Lobby request failed (curl=%d, http=%ld)\n", res, httpCode); fflush(stderr); } - } + + m_lobbyRequestInFlight = false; + }); } bool NGMP_OnlineServicesManager::sendChatMessage(const std::string& room, const std::string& message) { if (!m_isLoggedIn) { return false; } - fprintf(stderr, "[NGMP] Sending chat message in room '%s': %s\n", room.c_str(), message.c_str()); + if (m_chatSession) { + return m_chatSession->sendMessage(room, message); + } + fprintf(stderr, "[NGMP] sendChatMessage called but no active chat session\n"); fflush(stderr); - return true; + return false; } diff --git a/docs/WORKLOG/2026-08-DIARY.md b/docs/WORKLOG/2026-08-DIARY.md index 326ce08d1b3..7ecc2daebeb 100644 --- a/docs/WORKLOG/2026-08-DIARY.md +++ b/docs/WORKLOG/2026-08-DIARY.md @@ -16,3 +16,16 @@ - Wired lobby refresh hook in `WOLLobbyMenu.cpp`. - Hardcoded NGMP server host/port and SSL configuration at CMake compile-time (`cmake/ngmp.cmake` and `NGMP_Helpers.cpp`). Stripped all runtime `std::getenv` and local file reads from C++ client to conceal connection details in binary. Added `NGMP_USE_SSL` CMake option with CMake-time `ENV{NGMP_USE_SSL}` fallback. - Validated clean compilation and linking of `GeneralsXZH` executable on macOS Vulkan target (`macos-vulkan` preset). + +### Phase 2: NGMP Code Quality & WebSocket Chat + +- Code review of NGMP subsystem. Identified orphan declarations, duplicate code, synchronous blocking HTTP requests, and missing WebSocket implementation. +- Removed orphan `GetServerHTTPPort`/`GetServerSSLPort` declarations from `NGMP_Helpers.h` (functions deleted from `.cpp` in prior phase). +- Created `ngmp_curl_utils.h` with centralised `CurlResponse` + `WriteCallback` to eliminate duplication between `OnlineServices_Auth.cpp` and `OnlineServices_Manager.cpp`. +- Refactored `login()` → `loginAsync()`: HTTP request now runs on a dedicated `std::thread`, posting `EVENT_AUTH_SUCCESS`/`EVENT_AUTH_FAILURE` events when complete. Added `std::atomic m_loginInFlight` guard against concurrent requests. +- Refactored `requestLobbyList()` → `requestLobbyListAsync()`: same pattern. Lobby list written under event mutex before handoff to main thread. +- Implemented WebSocket chat via libcurl native WS API (`CURLOPT_CONNECT_ONLY` + `curl_ws_send`/`curl_ws_recv`): `NGMPChatSession` class manages the persistent connection on a dedicated receive thread; messages dispatched via callback to the NGMP event queue without touching UI. +- Updated `OnlineServices_Init.cpp` to connect `NGMPChatSession` on session restore and join/join all threads on shutdown. +- Added `requestLobbyListAsync()` call to `WOLLobbyMenuInit` under `#if SAGE_USE_NGMP` guard. +- Registered new sources (`OnlineServices_Chat.cpp`, `NGMPChatSession.h`, `ngmp_curl_utils.h`) in `GeneralsMD/Code/GameEngine/CMakeLists.txt`. +- Validated clean build and link of `GeneralsXZH` on `macos-vulkan` preset. From 1c106aaa1a0e063f2eb0024b7e87376472664b15 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Tue, 4 Aug 2026 18:18:39 -0300 Subject: [PATCH 07/51] fix(gui): bypass legacy gamespy timeout and hook ngmp login --- .../GUI/GUICallbacks/Menus/WOLLoginMenu.cpp | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp index 25d49d2daa2..d6a5b32db23 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp @@ -65,6 +65,12 @@ #include "GameNetwork/GameSpy/ThreadUtils.h" #include "GameNetwork/GameSpy/PersistentStorageThread.h" +#include "GameNetwork/RankPointValue.h" + +#if defined(SAGE_USE_NGMP) +#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" +#endif + #include "GameNetwork/GameSpyOverlay.h" #include "GameNetwork/WOLBrowser/WebBrowser.h" @@ -806,6 +812,19 @@ static void checkLogin() //------------------------------------------------------------------------------------------------- void WOLLoginMenuUpdate( WindowLayout * layout, void *userData) { +#if defined(SAGE_USE_NGMP) + if (NGMP_OnlineServicesManager::getInstance().isLoggedIn()) + { + if (!buttonPushed) + { + buttonPushed = true; + loginAttemptTime = 0; + nextScreen = "Menus/WOLWelcomeMenu.wnd"; + TheShell->pop(); + } + return; + } +#endif // We'll only be successful if we've requested to if(isShuttingDown && TheShell->isAnimFinished() && TheTransitionHandler->isFinished()) @@ -879,6 +898,7 @@ void WOLLoginMenuUpdate( WindowLayout * layout, void *userData) checkLogin(); } +#if !defined(SAGE_USE_NGMP) if (TheGameSpyInfo && !buttonPushed && loginAttemptTime && (loginAttemptTime + loginTimeoutInMS < timeGetTime())) { // timed out a login attempt, so say so @@ -898,6 +918,9 @@ void WOLLoginMenuUpdate( WindowLayout * layout, void *userData) TearDownGameSpy(); SetUpGameSpy( motd.str(), config.str() ); } +#else + loginAttemptTime = 0; +#endif } @@ -1346,6 +1369,10 @@ WindowMsgHandledType WOLLoginMenuSystem( GameWindow *window, UnsignedInt msg, if ( !email.isEmpty() && !login.isEmpty() && !password.isEmpty() ) { +#if defined(SAGE_USE_NGMP) + NGMP_OnlineServicesManager::getInstance().loginAsync(login.str(), password.str()); + loginAttemptTime = 0; +#else loginAttemptTime = timeGetTime(); BuddyRequest req; req.buddyRequestType = BuddyRequest::BUDDYREQUEST_LOGIN; @@ -1361,6 +1388,7 @@ WindowMsgHandledType WOLLoginMenuSystem( GameWindow *window, UnsignedInt msg, DEBUG_LOG(("before login: TheGameSpyInfo->stuff(%s/%s/%s)", TheGameSpyInfo->getLocalBaseName().str(), TheGameSpyInfo->getLocalEmail().str(), TheGameSpyInfo->getLocalPassword().str())); TheGameSpyBuddyMessageQueue->addRequest( req ); +#endif if(checkBoxRememberPassword && GadgetCheckBoxIsChecked(checkBoxRememberPassword)) { (*loginPref)["lastName"] = login; From f7a2380ef73940635352b8122d8c3aeab22aab42 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Tue, 4 Aug 2026 18:21:51 -0300 Subject: [PATCH 08/51] fix(gui): guard legacy gamespy disconnect popups under SAGE_USE_NGMP --- .../Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp | 2 ++ .../Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp index 69062298cff..3b5e41f350c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp @@ -1047,6 +1047,7 @@ void WOLLobbyMenuUpdate( WindowLayout * layout, void *userData) break; case PeerResponse::PEERRESPONSE_DISCONNECT: { +#if !defined(SAGE_USE_NGMP) sawImportantMessage = TRUE; UnicodeString title, body; AsciiString disconMunkee; @@ -1057,6 +1058,7 @@ void WOLLobbyMenuUpdate( WindowLayout * layout, void *userData) GSMessageBoxOk( title, body ); TheGameSpyInfo->reset(); TheShell->pop(); +#endif } break; case PeerResponse::PEERRESPONSE_CREATESTAGINGROOM: diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp index c84a8fd559e..050f0c26934 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp @@ -673,6 +673,7 @@ void WOLWelcomeMenuUpdate( WindowLayout * layout, void *userData) break; case PeerResponse::PEERRESPONSE_DISCONNECT: { +#if !defined(SAGE_USE_NGMP) sawImportantMessage = TRUE; UnicodeString title, body; AsciiString disconMunkee; @@ -682,6 +683,7 @@ void WOLWelcomeMenuUpdate( WindowLayout * layout, void *userData) GameSpyCloseAllOverlays(); GSMessageBoxOk( title, body ); TheShell->pop(); +#endif } break; } From 77262efa18df17b5956098f2a95f0efd671c27b2 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Tue, 4 Aug 2026 22:40:56 -0300 Subject: [PATCH 09/51] fix(ngmp): pump event queue in WOLLoginMenuUpdate to process auth events during shell --- .../GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp index d6a5b32db23..5305738b47f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp @@ -813,6 +813,11 @@ static void checkLogin() void WOLLoginMenuUpdate( WindowLayout * layout, void *userData) { #if defined(SAGE_USE_NGMP) + // Pump the NGMP event queue each frame so worker-thread events (e.g. EVENT_AUTH_SUCCESS) + // are delivered while we are in the shell menu. The normal GameEngine::update() path that + // pumps NGMP only runs during gameplay (inside VERIFY_CRC), not during shell menus. + NGMP_OnlineServicesManager::getInstance().update(); + if (NGMP_OnlineServicesManager::getInstance().isLoggedIn()) { if (!buttonPushed) From e46337eff6c63d5d1d3bfb946f4ef3e866d42900 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Tue, 4 Aug 2026 22:53:22 -0300 Subject: [PATCH 10/51] feat(ngmp): implement UI binding for lobby list and chat // GeneralsX @feature GeneralsOnline NGMP UI Binding - Added RefreshNGMPGameListBoxes to LobbyUtils for populating the UI - Added chat session initialization upon EVENT_AUTH_SUCCESS - Hooked WOLLobbyMenuUpdate to poll NGMP events instead of GameSpy - Replaced TheGameSpyInfo->sendChat with NGMP sendChatMessage - Modified OnlineServices_Manager update() to return events (pollEvents) - Ensured shell event pump mechanism is using pollEvents --- .../Include/GameNetwork/GameSpy/LobbyUtils.h | 1 + .../Source/GameNetwork/GameSpy/LobbyUtils.cpp | 46 +++++++++++++++++++ .../GeneralsOnline/OnlineServices_Manager.h | 2 +- .../GameEngine/Source/Common/GameEngine.cpp | 2 +- .../GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp | 22 +++++++++ .../GUI/GUICallbacks/Menus/WOLLoginMenu.cpp | 2 +- .../GeneralsOnline/OnlineServices_Manager.cpp | 12 ++++- 7 files changed, 82 insertions(+), 5 deletions(-) diff --git a/Core/GameEngine/Include/GameNetwork/GameSpy/LobbyUtils.h b/Core/GameEngine/Include/GameNetwork/GameSpy/LobbyUtils.h index bb07bc2f899..0ef9c61b4c6 100644 --- a/Core/GameEngine/Include/GameNetwork/GameSpy/LobbyUtils.h +++ b/Core/GameEngine/Include/GameNetwork/GameSpy/LobbyUtils.h @@ -38,6 +38,7 @@ void GrabWindowInfo(); void ReleaseWindowInfo(); void RefreshGameInfoListBox( GameWindow *mainWin, GameWindow *win ); void RefreshGameListBoxes(); +void RefreshNGMPGameListBoxes(const std::vector& lobbies); void ToggleGameListType(); void playerTemplateComboBoxTooltip(GameWindow *wndComboBox, WinInstanceData *instData, UnsignedInt mouse); diff --git a/Core/GameEngine/Source/GameNetwork/GameSpy/LobbyUtils.cpp b/Core/GameEngine/Source/GameNetwork/GameSpy/LobbyUtils.cpp index f8b8448f317..708af93bc5b 100644 --- a/Core/GameEngine/Source/GameNetwork/GameSpy/LobbyUtils.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameSpy/LobbyUtils.cpp @@ -61,6 +61,10 @@ #include "GameNetwork/GameSpy/PersistentStorageDefs.h" #include "GameNetwork/GameSpy/GSConfig.h" +#if defined(SAGE_USE_NGMP) +#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" +#endif + #include "Common/STLTypedefs.h" @@ -866,6 +870,48 @@ void RefreshGameListBoxes() } } +void RefreshNGMPGameListBoxes(const std::vector& lobbies) +{ + GameWindow *win = GetGameListBox(); + if (!win) + return; + + // clear it out + GadgetListBoxReset(win); + + Color gameColor = GameSpyColor[GSCOLOR_GAME]; + + for (const auto& lobby : lobbies) + { + AsciiString asciiName(lobby.name.c_str()); + UnicodeString uName; + uName.translate(asciiName); + + Int index = GadgetListBoxAddEntryText(win, uName, gameColor, -1, COLUMN_NAME); + // Assuming we can use a hash or fake id for the listbox user data since we don't have integer IDs + // Let's just put 0 for now since NGMP lobbies use string IDs (but the UI expects an Int). + // A cleaner solution later would be to map string IDs to integer handles. + GadgetListBoxSetItemData(win, reinterpret_cast(std::uintptr_t(1)), index); + + AsciiString asciiMap(lobby.mapName.c_str()); + UnicodeString uMap; + uMap.translate(asciiMap); + + GadgetListBoxAddEntryText(win, uMap, gameColor, index, COLUMN_MAP); + + // Ladder info usually goes here, but we can just leave it blank for now + GadgetListBoxAddEntryText(win, L" ", gameColor, index, COLUMN_LADDER); + + UnicodeString playersStr; + playersStr.format(L"%d/%d", lobby.currentPlayers, lobby.maxPlayers); + GadgetListBoxAddEntryText(win, playersStr, gameColor, index, COLUMN_NUMPLAYERS); + + GadgetListBoxAddEntryText(win, L" ", gameColor, index, COLUMN_PASSWORD); // No password for now + } + + // Update game info list box if we had one +} + void ToggleGameListType() { isSmall = !isSmall; diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h index 9f24b6a8b31..989465e6655 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h @@ -42,7 +42,7 @@ class NGMP_OnlineServicesManager { static NGMP_OnlineServicesManager& getInstance(); bool init(); - void update(); // Main thread UI tick dispatch + std::vector pollEvents(); // Main thread UI tick dispatch void shutdown(); // Async login — result delivered via EVENT_AUTH_SUCCESS / EVENT_AUTH_FAILURE diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index 2277bd21dc3..12d4f2cd2f9 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -1019,7 +1019,7 @@ void GameEngine::update() } #ifdef SAGE_USE_NGMP - NGMP_OnlineServicesManager::getInstance().update(); + NGMP_OnlineServicesManager::getInstance().pollEvents(); #endif } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp index 3b5e41f350c..e6d7cc707e1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp @@ -941,6 +941,22 @@ void WOLLobbyMenuUpdate( WindowLayout * layout, void *userData) raiseMessageBoxes = false; } +#if defined(SAGE_USE_NGMP) + // Process NGMP Events + auto events = NGMP_OnlineServicesManager::getInstance().pollEvents(); + for (const auto& ev : events) { + if (ev.type == NGMPEvent::EVENT_LOBBY_LIST_UPDATED) { + RefreshNGMPGameListBoxes(NGMP_OnlineServicesManager::getInstance().getLobbies()); + } + else if (ev.type == NGMPEvent::EVENT_CHAT_MESSAGE_RECEIVED) { + AsciiString msg(ev.payload.c_str()); + UnicodeString uMsg; + uMsg.translate(msg); + TheGameSpyInfo->addText(uMsg, GameSpyColor[GSCOLOR_DEFAULT], nullptr); + } + } +#endif + if (TheShell->isAnimFinished() && TheTransitionHandler->isFinished() && !buttonPushed && TheGameSpyPeerMessageQueue) { HandleBuddyResponses(); @@ -1873,7 +1889,13 @@ WindowMsgHandledType WOLLobbyMenuSystem( GameWindow *window, UnsignedInt msg, // Send the message if (!handleLobbySlashCommands(txtInput)) { +#if defined(SAGE_USE_NGMP) + AsciiString msg; + msg.translate(txtInput); + NGMP_OnlineServicesManager::getInstance().sendChatMessage("lobby", msg.str()); +#else TheGameSpyInfo->sendChat( txtInput, false, listboxLobbyPlayers ); +#endif } } break; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp index 5305738b47f..04cf4eda862 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp @@ -816,7 +816,7 @@ void WOLLoginMenuUpdate( WindowLayout * layout, void *userData) // Pump the NGMP event queue each frame so worker-thread events (e.g. EVENT_AUTH_SUCCESS) // are delivered while we are in the shell menu. The normal GameEngine::update() path that // pumps NGMP only runs during gameplay (inside VERIFY_CRC), not during shell menus. - NGMP_OnlineServicesManager::getInstance().update(); + NGMP_OnlineServicesManager::getInstance().pollEvents(); if (NGMP_OnlineServicesManager::getInstance().isLoggedIn()) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp index 0e27495ffbc..600cd29ffa7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp @@ -4,6 +4,7 @@ #include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" #include "GameNetwork/GeneralsOnline/NGMP_Helpers.h" #include "GameNetwork/GeneralsOnline/ngmp_curl_utils.h" +#include "GameNetwork/GeneralsOnline/NGMPChatSession.h" #include #include #include @@ -26,13 +27,14 @@ void NGMP_OnlineServicesManager::postEvent(const NGMPEvent& event) { m_eventQueue.push(event); } -void NGMP_OnlineServicesManager::update() { +std::vector NGMP_OnlineServicesManager::pollEvents() { std::queue pendingEvents; { std::lock_guard lock(m_eventMutex); std::swap(pendingEvents, m_eventQueue); } + std::vector events; while (!pendingEvents.empty()) { NGMPEvent ev = pendingEvents.front(); pendingEvents.pop(); @@ -40,6 +42,11 @@ void NGMP_OnlineServicesManager::update() { switch (ev.type) { case NGMPEvent::EVENT_AUTH_SUCCESS: fprintf(stderr, "[NGMP-MainThread] Event: Auth Success\n"); + m_isLoggedIn = true; + if (!m_chatSession) { + m_chatSession.reset(new NGMP::NGMPChatSession()); + } + m_chatSession->connect(NGMP::GetServerWSEndpoint() + "/chat", m_authToken); break; case NGMPEvent::EVENT_AUTH_FAILURE: fprintf(stderr, "[NGMP-MainThread] Event: Auth Failure: %s\n", ev.payload.c_str()); @@ -59,8 +66,9 @@ void NGMP_OnlineServicesManager::update() { default: break; } - fflush(stderr); + events.push_back(ev); } + return events; } void NGMP_OnlineServicesManager::requestLobbyListAsync() { From bae2778e2cf961f717cb750dd3baeda6b78803cd Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 5 Aug 2026 00:06:01 -0300 Subject: [PATCH 11/51] fix(ngmp): load endpoint config from env vars at runtime --- .../GUI/GUICallbacks/Menus/WOLLoginMenu.cpp | 15 +++++++++++- .../GeneralsOnline/NGMP_Helpers.cpp | 23 +++++++++++++++---- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp index 04cf4eda862..66f687b6164 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp @@ -816,7 +816,7 @@ void WOLLoginMenuUpdate( WindowLayout * layout, void *userData) // Pump the NGMP event queue each frame so worker-thread events (e.g. EVENT_AUTH_SUCCESS) // are delivered while we are in the shell menu. The normal GameEngine::update() path that // pumps NGMP only runs during gameplay (inside VERIFY_CRC), not during shell menus. - NGMP_OnlineServicesManager::getInstance().pollEvents(); + auto events = NGMP_OnlineServicesManager::getInstance().pollEvents(); if (NGMP_OnlineServicesManager::getInstance().isLoggedIn()) { @@ -829,12 +829,24 @@ void WOLLoginMenuUpdate( WindowLayout * layout, void *userData) } return; } + + // Process any other NGMP events + for (const auto& ev : events) { + if (ev.type == NGMPEvent::EVENT_AUTH_FAILURE) { + loginAttemptTime = 0; + EnableLoginControls(TRUE); + GSMessageBoxOk(TheGameText->fetch("GUI:ConnectionErrorTitle"), TheGameText->fetch("GUI:ConnectionError")); + fprintf(stderr, "[NGMP] Showing connection error to user: %s\n", ev.payload.c_str()); + fflush(stderr); + } + } #endif // We'll only be successful if we've requested to if(isShuttingDown && TheShell->isAnimFinished() && TheTransitionHandler->isFinished()) shutdownComplete(layout); +#if !defined(SAGE_USE_NGMP) if (TheShell->isAnimFinished() && !buttonPushed && TheGameSpyPeerMessageQueue) { PingResponse pingResp; @@ -902,6 +914,7 @@ void WOLLoginMenuUpdate( WindowLayout * layout, void *userData) checkLogin(); } +#endif #if !defined(SAGE_USE_NGMP) if (TheGameSpyInfo && !buttonPushed && loginAttemptTime && (loginAttemptTime + loginTimeoutInMS < timeGetTime())) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp index 3784218f10e..f3a0bae82d3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp @@ -21,6 +21,11 @@ namespace NGMP { bool IsSSLEnabled() { + const char* envSSL = std::getenv("NGMP_USE_SSL"); + if (envSSL) { + std::string s(envSSL); + return s == "ON" || s == "1" || s == "true" || s == "TRUE"; + } #if defined(NGMP_USE_SSL) && NGMP_USE_SSL return true; #else @@ -78,17 +83,27 @@ std::string LoadAuthToken() { } std::string GetServerWSEndpoint() { + const char* envHost = std::getenv("NGMP_DEFAULT_HOST"); + const char* envPort = std::getenv("NGMP_SERVER_PORT"); + std::string host = envHost ? envHost : NGMP_DEFAULT_HOST; + std::string port = envPort ? envPort : NGMP_DEFAULT_PORT; + if (IsSSLEnabled()) { - return "wss://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/ws"; + return "wss://" + host + ":" + port + "/ws"; } - return "ws://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/ws"; + return "ws://" + host + ":" + port + "/ws"; } std::string GetServerRESTEndpoint() { + const char* envHost = std::getenv("NGMP_DEFAULT_HOST"); + const char* envPort = std::getenv("NGMP_SERVER_PORT"); + std::string host = envHost ? envHost : NGMP_DEFAULT_HOST; + std::string port = envPort ? envPort : NGMP_DEFAULT_PORT; + if (IsSSLEnabled()) { - return "https://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/api"; + return "https://" + host + ":" + port + "/api"; } - return "http://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/api"; + return "http://" + host + ":" + port + "/api"; } } // namespace NGMP From 2aac27da35d7a794992f3ae942ace564fb5db405 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 5 Aug 2026 00:08:47 -0300 Subject: [PATCH 12/51] fix(ngmp): inject endpoint configuration via CMake compile definitions --- .../GeneralsOnline/NGMP_Helpers.cpp | 23 +++------------- cmake/config-build.cmake | 26 ++++++++++++++++++- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp index f3a0bae82d3..3784218f10e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp @@ -21,11 +21,6 @@ namespace NGMP { bool IsSSLEnabled() { - const char* envSSL = std::getenv("NGMP_USE_SSL"); - if (envSSL) { - std::string s(envSSL); - return s == "ON" || s == "1" || s == "true" || s == "TRUE"; - } #if defined(NGMP_USE_SSL) && NGMP_USE_SSL return true; #else @@ -83,27 +78,17 @@ std::string LoadAuthToken() { } std::string GetServerWSEndpoint() { - const char* envHost = std::getenv("NGMP_DEFAULT_HOST"); - const char* envPort = std::getenv("NGMP_SERVER_PORT"); - std::string host = envHost ? envHost : NGMP_DEFAULT_HOST; - std::string port = envPort ? envPort : NGMP_DEFAULT_PORT; - if (IsSSLEnabled()) { - return "wss://" + host + ":" + port + "/ws"; + return "wss://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/ws"; } - return "ws://" + host + ":" + port + "/ws"; + return "ws://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/ws"; } std::string GetServerRESTEndpoint() { - const char* envHost = std::getenv("NGMP_DEFAULT_HOST"); - const char* envPort = std::getenv("NGMP_SERVER_PORT"); - std::string host = envHost ? envHost : NGMP_DEFAULT_HOST; - std::string port = envPort ? envPort : NGMP_DEFAULT_PORT; - if (IsSSLEnabled()) { - return "https://" + host + ":" + port + "/api"; + return "https://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/api"; } - return "http://" + host + ":" + port + "/api"; + return "http://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/api"; } } // namespace NGMP diff --git a/cmake/config-build.cmake b/cmake/config-build.cmake index 82d99e947e6..452e44692ce 100644 --- a/cmake/config-build.cmake +++ b/cmake/config-build.cmake @@ -142,7 +142,31 @@ add_feature_info(NGMPProtocol SAGE_USE_NGMP "Using NGMP multiplayer protocol (Ge if(SAGE_USE_NGMP) target_compile_definitions(core_config INTERFACE SAGE_USE_NGMP) - message(STATUS "NGMP (GeneralsOnline) multiplayer protocol enabled") + + set(NGMP_DEFAULT_HOST $ENV{NGMP_DEFAULT_HOST} CACHE STRING "NGMP default host") + if(NOT NGMP_DEFAULT_HOST) + set(NGMP_DEFAULT_HOST "localhost") + endif() + + set(NGMP_SERVER_PORT $ENV{NGMP_SERVER_PORT} CACHE STRING "NGMP server port") + if(NOT NGMP_SERVER_PORT) + set(NGMP_SERVER_PORT "9001") + endif() + + set(NGMP_USE_SSL $ENV{NGMP_USE_SSL} CACHE STRING "NGMP use SSL") + if(NOT NGMP_USE_SSL) + set(NGMP_USE_SSL "OFF") + endif() + + target_compile_definitions(core_config INTERFACE + NGMP_DEFAULT_HOST="${NGMP_DEFAULT_HOST}" + NGMP_DEFAULT_PORT="${NGMP_SERVER_PORT}" + ) + if(NGMP_USE_SSL STREQUAL "ON" OR NGMP_USE_SSL STREQUAL "1" OR NGMP_USE_SSL STREQUAL "TRUE") + target_compile_definitions(core_config INTERFACE NGMP_USE_SSL=1) + endif() + + message(STATUS "NGMP (GeneralsOnline) multiplayer protocol enabled [Host: ${NGMP_DEFAULT_HOST}:${NGMP_SERVER_PORT}]") endif() if(SAGE_USE_GLM) From 801c8d64d549597271cecf93ef4c9b74d2c60e1c Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 5 Aug 2026 18:51:04 -0300 Subject: [PATCH 13/51] fix(ngmp): bypass GameSpy patch check when NGMP is enabled --- .../Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp | 4 ++++ .../Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp index 1c68344f8c0..39ffb1b8f4c 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp @@ -1460,7 +1460,11 @@ WindowMsgHandledType MainMenuSystem( GameWindow *window, UnsignedInt msg, dropDownWindows[DROPDOWN_MULTIPLAYER]->winHide(FALSE); TheTransitionHandler->reverse("MainMenuMultiPlayerMenuTransitionToNext"); +#if defined(SAGE_USE_NGMP) + TheShell->push( "Menus/GameSpyLoginProfile.wnd" ); +#else StartPatchCheck(); +#endif // localAnimateWindowManager->reverseAnimateWindow(); dropDown = DROPDOWN_NONE; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp index 4319b591fb4..e43ddc74aef 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp @@ -1544,7 +1544,11 @@ WindowMsgHandledType MainMenuSystem( GameWindow *window, UnsignedInt msg, dropDownWindows[DROPDOWN_MULTIPLAYER]->winHide(FALSE); TheTransitionHandler->reverse("MainMenuMultiPlayerMenuTransitionToNext"); +#if defined(SAGE_USE_NGMP) + TheShell->push( "Menus/GameSpyLoginProfile.wnd" ); +#else StartPatchCheck(); +#endif // localAnimateWindowManager->reverseAnimateWindow(); dropDown = DROPDOWN_NONE; From 411d46b771e86dc9abaf8131920b0cd65a120c3d Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 5 Aug 2026 20:52:33 -0300 Subject: [PATCH 14/51] fix: UB in obfuscate() crashing on ARM64 --- .../GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp | 8 +++++--- .../GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp index 6edaba07101..21a2e7e0bac 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp @@ -123,9 +123,11 @@ static AsciiString obfuscate( AsciiString in ) if (!*c2) c2 = xorWord; if (*c != *c2) - *c = *c++ ^ *c2++; - else - c++, c2++; + { + *c = *c ^ *c2; + } + c++; + c2++; } AsciiString out = buf; delete[] buf; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp index 66f687b6164..34daaff91a1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp @@ -129,9 +129,11 @@ static AsciiString obfuscate( AsciiString in ) if (!*c2) c2 = xorWord; if (*c != *c2) - *c = *c++ ^ *c2++; - else - c++, c2++; + { + *c = *c ^ *c2; + } + c++; + c2++; } AsciiString out = buf; delete[] buf; From efd9493fc972061bc19c836eb0e52d7883af3976 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 5 Aug 2026 21:06:48 -0300 Subject: [PATCH 15/51] feat(ngmp): browser gamecode login flow via SDL_OpenURL + CheckLogin polling --- .gitmodules | 6 + .../GameNetwork/GeneralsOnline/NGMP_Helpers.h | 17 +- .../GeneralsOnline/OnlineServices_Manager.h | 23 +- .../GUI/GUICallbacks/Menus/MainMenu.cpp | 9 +- .../GeneralsOnline/NGMP_Helpers.cpp | 71 +++++- .../GeneralsOnline/OnlineServices_Auth.cpp | 233 ++++++++++++------ .../GeneralsOnline/OnlineServices_Init.cpp | 39 ++- references/GameClient | 1 + references/GameServer | 1 + 9 files changed, 293 insertions(+), 107 deletions(-) create mode 160000 references/GameClient create mode 160000 references/GameServer diff --git a/.gitmodules b/.gitmodules index e0ad1191453..59d44cf796c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,9 @@ path = references/fbraz3-dxvk url = https://github.com/fbraz3/dxvk.git branch = generalsx-macos-v2.6 +[submodule "references/GameClient"] + path = references/GameClient + url = https://github.com/GeneralsOnlineDevelopmentTeam/GameClient.git +[submodule "references/GameServer"] + path = references/GameServer + url = https://github.com/GeneralsOnlineDevelopmentTeam/Services.git diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h index f526380be21..29cb655d6e5 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h @@ -24,15 +24,30 @@ bool SaveAuthToken(const std::string& token); // Loads authentication token from local user storage std::string LoadAuthToken(); +// Saves refresh token to local user storage +bool SaveRefreshToken(const std::string& token); + +// Loads refresh token from local user storage +std::string LoadRefreshToken(); + // Returns true if SSL (HTTPS/WSS) is enabled bool IsSSLEnabled(); // Returns default server WS endpoint URL std::string GetServerWSEndpoint(); -// Returns default server REST endpoint URL +// Returns default server REST endpoint URL (e.g. http://host:port) std::string GetServerRESTEndpoint(); +// Returns a full named API endpoint URL (e.g. /env/dev/contract/1/CheckLogin) +std::string GetAPIEndpoint(const char* szEndpoint); + +// Returns the browser login URL for a given gamecode +std::string GetBrowserLoginURL(const std::string& gamecode); + +// Generates a random 32-char alphanumeric gamecode +std::string GenerateGamecode(); + } // namespace NGMP #endif // NGMP_HELPERS_H diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h index 989465e6655..14d7680962c 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h @@ -12,12 +12,14 @@ #include #include #include +#include struct NGMPEvent { enum Type { EVENT_NONE, EVENT_AUTH_SUCCESS, EVENT_AUTH_FAILURE, + EVENT_AUTH_CANCELLED, EVENT_LOBBY_LIST_UPDATED, EVENT_CHAT_MESSAGE_RECEIVED, EVENT_CHAT_CONNECTED, @@ -45,9 +47,15 @@ class NGMP_OnlineServicesManager { std::vector pollEvents(); // Main thread UI tick dispatch void shutdown(); - // Async login — result delivered via EVENT_AUTH_SUCCESS / EVENT_AUTH_FAILURE - void loginAsync(const std::string& username, const std::string& password); - bool loginWithToken(const std::string& token); + // Browser-based gamecode login flow (macOS/Linux: uses SDL_OpenURL) + void beginBrowserLogin(); + void cancelBrowserLogin(); + // Call from main-thread update loop while waiting for browser login + void tickBrowserLogin(); + + // Token-based silent re-login + void loginWithRefreshToken(const std::string& refreshToken); + void logout(); // Async lobby fetch — result delivered via EVENT_LOBBY_LIST_UPDATED @@ -76,9 +84,12 @@ class NGMP_OnlineServicesManager { std::string m_authToken; std::vector m_lobbies; - // Async login state - std::atomic m_loginInFlight = false; - std::thread m_loginThread; + // Browser-based login state + std::atomic m_waitingBrowserLogin = false; + std::string m_gamecode; + std::chrono::steady_clock::time_point m_lastPollTime; + std::thread m_pollThread; + std::atomic m_pollThreadRunning = false; // Async lobby request state std::atomic m_lobbyRequestInFlight = false; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp index e43ddc74aef..9f370d42018 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp @@ -82,6 +82,10 @@ #include #endif +// GeneralsX @feature GeneralsOnline NGMP browser-based login +#ifdef SAGE_USE_NGMP +#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" +#endif // PRIVATE DATA /////////////////////////////////////////////////////////////////////////////////// @@ -1545,7 +1549,10 @@ WindowMsgHandledType MainMenuSystem( GameWindow *window, UnsignedInt msg, TheTransitionHandler->reverse("MainMenuMultiPlayerMenuTransitionToNext"); #if defined(SAGE_USE_NGMP) - TheShell->push( "Menus/GameSpyLoginProfile.wnd" ); + // GeneralsX @feature GeneralsOnline - Browser gamecode login flow + // No in-game login form; the NGMP manager opens the browser and polls. + NGMP_OnlineServicesManager::getInstance().init(); + NGMP_OnlineServicesManager::getInstance().beginBrowserLogin(); #else StartPatchCheck(); #endif diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp index 3784218f10e..650a2c384b0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #ifndef NGMP_DEFAULT_HOST #define NGMP_DEFAULT_HOST "localhost" @@ -18,6 +20,18 @@ #define NGMP_DEFAULT_PORT "9001" #endif +#ifndef NGMP_SERVER_ENV +#define NGMP_SERVER_ENV "dev" +#endif + +#ifndef NGMP_CONTRACT_VERSION +#define NGMP_CONTRACT_VERSION "1" +#endif + +#ifndef NGMP_CLIENT_ID +#define NGMP_CLIENT_ID "GeneralsXZH" +#endif + namespace NGMP { bool IsSSLEnabled() { @@ -28,8 +42,6 @@ bool IsSSLEnabled() { #endif } - - uint32_t GetTicks() { auto now = std::chrono::steady_clock::now(); return static_cast( @@ -77,6 +89,31 @@ std::string LoadAuthToken() { return token; } +bool SaveRefreshToken(const std::string& token) { + std::string path = GetStoragePath(); + std::filesystem::create_directories(path); + std::string tokenFile = path + "refresh.token"; + std::ofstream out(tokenFile, std::ios::out | std::ios::trunc); + if (!out.is_open()) { + return false; + } + out << token; + out.close(); + return true; +} + +std::string LoadRefreshToken() { + std::string path = GetStoragePath(); + std::string tokenFile = path + "refresh.token"; + std::ifstream in(tokenFile); + if (!in.is_open()) { + return ""; + } + std::string token; + in >> token; + return token; +} + std::string GetServerWSEndpoint() { if (IsSSLEnabled()) { return "wss://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/ws"; @@ -86,9 +123,35 @@ std::string GetServerWSEndpoint() { std::string GetServerRESTEndpoint() { if (IsSSLEnabled()) { - return "https://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/api"; + return "https://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT); + } + return "http://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT); +} + +std::string GetAPIEndpoint(const char* szEndpoint) { + return std::format("{}/env/" NGMP_SERVER_ENV "/contract/" NGMP_CONTRACT_VERSION "/{}", + GetServerRESTEndpoint(), szEndpoint); +} + +std::string GetBrowserLoginURL(const std::string& gamecode) { + // Use the web portal on the server for browser-based OAuth login + return std::format("{}/login/?gamecode={}", GetServerRESTEndpoint(), gamecode); +} + +std::string GenerateGamecode() { + const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const size_t max_index = sizeof(charset) - 2; // -2: skip null terminator + + auto seed = std::chrono::system_clock::now().time_since_epoch().count(); + std::mt19937 generator(static_cast(seed)); + std::uniform_int_distribution distribution(0, max_index); + + std::string result; + result.reserve(32); + for (int i = 0; i < 32; ++i) { + result += charset[distribution(generator)]; } - return "http://" + std::string(NGMP_DEFAULT_HOST) + ":" + std::string(NGMP_DEFAULT_PORT) + "/api"; + return result; } } // namespace NGMP diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp index 73a5cfb38bb..6d341d94208 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp @@ -1,123 +1,214 @@ // GeneralsX @feature GeneralsOnline NGMP Auth implementation -// Handles user authentication and JWT session token persistence. +// Browser-based gamecode login flow (macOS/Linux via SDL_OpenURL). +// Mirrors GeneralsOnline reference: references/GameClient/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp #include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" #include "GameNetwork/GeneralsOnline/NGMP_Helpers.h" #include "GameNetwork/GeneralsOnline/ngmp_curl_utils.h" +#include #include #include +#include #include using json = nlohmann::json; -void NGMP_OnlineServicesManager::loginAsync(const std::string& username, const std::string& password) { - if (m_loginInFlight.exchange(true)) { - fprintf(stderr, "[NGMP] Login already in flight, ignoring duplicate request\n"); +// Server-side result enum matching GenOnlineService EPendingLoginState +enum class ELoginPollResult : int { + CODE_INVALID = -1, + WAITING = 0, + LOGIN_SUCCESS = 1, + LOGIN_FAILED = 2 +}; + +// ────────────────────────────────────────────────────────────────────────────── +// beginBrowserLogin +// Generates a gamecode, opens the web portal in the system browser (SDL_OpenURL), +// shows a "please continue in your browser" message, and starts polling. +// ────────────────────────────────────────────────────────────────────────────── +void NGMP_OnlineServicesManager::beginBrowserLogin() { + if (m_waitingBrowserLogin.exchange(true)) { + fprintf(stderr, "[NGMP] Browser login already in progress\n"); fflush(stderr); return; } - if (m_loginThread.joinable()) { - m_loginThread.join(); + m_gamecode = NGMP::GenerateGamecode(); + std::string loginURL = NGMP::GetBrowserLoginURL(m_gamecode); + + fprintf(stderr, "[NGMP] beginBrowserLogin: gamecode=%s url=%s\n", + m_gamecode.c_str(), loginURL.c_str()); + fflush(stderr); + + // Open the browser so the user can authenticate + if (!SDL_OpenURL(loginURL.c_str())) { + fprintf(stderr, "[NGMP] SDL_OpenURL failed: %s\n", SDL_GetError()); + fflush(stderr); + } + + // Start background polling thread + m_pollThreadRunning = true; + if (m_pollThread.joinable()) { + m_pollThread.join(); } - m_loginThread = std::thread([this, username, password]() { - fprintf(stderr, "[NGMP] Attempting login for user: %s\n", username.c_str()); + m_pollThread = std::thread([this]() { + const int64_t pollIntervalMs = 1000; + + fprintf(stderr, "[NGMP] Poll thread started for gamecode=%s\n", m_gamecode.c_str()); fflush(stderr); - CURL* curl = curl_easy_init(); - if (!curl) { - fprintf(stderr, "[NGMP] Failed to initialize libcurl for auth\n"); - fflush(stderr); - m_loginInFlight = false; - NGMPEvent ev; - ev.type = NGMPEvent::EVENT_AUTH_FAILURE; - ev.payload = "Failed to initialize HTTP client"; - postEvent(ev); - return; - } + while (m_pollThreadRunning && m_waitingBrowserLogin) { + std::this_thread::sleep_for(std::chrono::milliseconds(pollIntervalMs)); + + if (!m_pollThreadRunning || !m_waitingBrowserLogin) { + break; + } + + // POST /env/dev/contract/1/CheckLogin + std::string url = NGMP::GetAPIEndpoint("CheckLogin"); + + json requestJson = { + { "code", m_gamecode }, + { "client_id", NGMP_CLIENT_ID }, + { "reserved_0", "" }, + { "reserved_1", "" }, + { "reserved_2", "" } + }; + std::string requestBody = requestJson.dump(); + + CURL* curl = curl_easy_init(); + if (!curl) { + fprintf(stderr, "[NGMP] curl_easy_init failed in poll thread\n"); + fflush(stderr); + continue; + } + + NGMP::Internal::CurlResponse response; + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); - std::string url = NGMP::GetServerRESTEndpoint() + "/auth/login"; - json requestJson = { - {"username", username}, - {"password", password} - }; - std::string requestBody = requestJson.dump(); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, requestBody.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NGMP::Internal::WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); - NGMP::Internal::CurlResponse response; - struct curl_slist* headers = nullptr; - headers = curl_slist_append(headers, "Content-Type: application/json"); + CURLcode res = curl_easy_perform(curl); + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, requestBody.c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NGMP::Internal::WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + curl_slist_free_all(headers); + curl_easy_cleanup(curl); - CURLcode res = curl_easy_perform(curl); - long httpCode = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + if (res != CURLE_OK) { + fprintf(stderr, "[NGMP] Poll HTTP error: %s\n", curl_easy_strerror(res)); + fflush(stderr); + continue; + } - curl_slist_free_all(headers); - curl_easy_cleanup(curl); + fprintf(stderr, "[NGMP] CheckLogin response (%ld): %s\n", httpCode, response.text.c_str()); + fflush(stderr); - if (res == CURLE_OK && (httpCode == 200 || httpCode == 201)) { try { - auto responseJson = json::parse(response.text); - if (responseJson.contains("token")) { - std::string token = responseJson["token"].get(); + auto respJson = json::parse(response.text); + int resultCode = respJson.value("result", -1); + ELoginPollResult pollResult = static_cast(resultCode); - // Update state (write from worker thread, read-only from main thread until event arrives) - m_authToken = token; - m_username = username; + if (pollResult == ELoginPollResult::WAITING) { + fprintf(stderr, "[NGMP] Waiting for user to authenticate in browser...\n"); + fflush(stderr); + continue; + } + else if (pollResult == ELoginPollResult::CODE_INVALID) { + fprintf(stderr, "[NGMP] Gamecode not recognized by server yet, retrying...\n"); + fflush(stderr); + continue; + } + else if (pollResult == ELoginPollResult::LOGIN_SUCCESS) { + std::string sessionToken = respJson.value("session_token", ""); + std::string refreshToken = respJson.value("refresh_token", ""); + std::string displayName = respJson.value("display_name", ""); + int64_t userId = respJson.value("user_id", int64_t(-1)); + + m_authToken = sessionToken; + m_username = displayName; m_isLoggedIn = true; - NGMP::SaveAuthToken(token); + NGMP::SaveAuthToken(sessionToken); + NGMP::SaveRefreshToken(refreshToken); + + m_waitingBrowserLogin = false; + m_pollThreadRunning = false; + + fprintf(stderr, "[NGMP] Login successful! user=%s id=%lld\n", + displayName.c_str(), (long long)userId); + fflush(stderr); NGMPEvent ev; - ev.type = NGMPEvent::EVENT_AUTH_SUCCESS; - ev.payload = token; + ev.type = NGMPEvent::EVENT_AUTH_SUCCESS; + ev.payload = sessionToken; postEvent(ev); - - fprintf(stderr, "[NGMP] Login successful for user: %s\n", username.c_str()); + return; + } + else if (pollResult == ELoginPollResult::LOGIN_FAILED) { + fprintf(stderr, "[NGMP] Server reported login failure\n"); fflush(stderr); - m_loginInFlight = false; + + m_waitingBrowserLogin = false; + m_pollThreadRunning = false; + + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_AUTH_FAILURE; + ev.payload = "Login failed"; + postEvent(ev); return; } - } catch (const std::exception& e) { - fprintf(stderr, "[NGMP] JSON parse exception during auth: %s\n", e.what()); + } + catch (const std::exception& e) { + fprintf(stderr, "[NGMP] CheckLogin JSON parse error: %s\n", e.what()); fflush(stderr); } } - // Auth failed - NGMPEvent ev; - ev.type = NGMPEvent::EVENT_AUTH_FAILURE; - ev.payload = "Invalid credentials or server unavailable"; - postEvent(ev); - - fprintf(stderr, "[NGMP] Login failed for user %s (HTTP %ld)\n", username.c_str(), httpCode); + fprintf(stderr, "[NGMP] Poll thread exiting\n"); fflush(stderr); - m_loginInFlight = false; }); } -bool NGMP_OnlineServicesManager::loginWithToken(const std::string& token) { - if (token.empty()) { - return false; +// ────────────────────────────────────────────────────────────────────────────── +// cancelBrowserLogin — called when the user clicks Cancel in the in-game dialog +// ────────────────────────────────────────────────────────────────────────────── +void NGMP_OnlineServicesManager::cancelBrowserLogin() { + if (!m_waitingBrowserLogin) return; + + fprintf(stderr, "[NGMP] Browser login cancelled by user\n"); + fflush(stderr); + + m_pollThreadRunning = false; + m_waitingBrowserLogin = false; + + if (m_pollThread.joinable()) { + m_pollThread.join(); } - m_authToken = token; - m_isLoggedIn = true; NGMPEvent ev; - ev.type = NGMPEvent::EVENT_AUTH_SUCCESS; - ev.payload = token; + ev.type = NGMPEvent::EVENT_AUTH_CANCELLED; postEvent(ev); +} + +// ────────────────────────────────────────────────────────────────────────────── +// loginWithRefreshToken — silent token re-login (no browser required) +// ────────────────────────────────────────────────────────────────────────────── +void NGMP_OnlineServicesManager::loginWithRefreshToken(const std::string& refreshToken) { + if (refreshToken.empty()) return; - fprintf(stderr, "[NGMP] Authenticated via saved token\n"); + // TODO: implement POST /LoginWithToken with the refresh token for silent re-auth. + // For now, treat a non-empty saved refresh token as requiring a fresh browser login. + fprintf(stderr, "[NGMP] Saved refresh token found but silent re-login not yet implemented; starting browser flow\n"); fflush(stderr); - return true; + beginBrowserLogin(); } void NGMP_OnlineServicesManager::logout() { diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp index 4f73dbfa6d3..ab9e3f9a5e9 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -10,28 +10,16 @@ bool NGMP_OnlineServicesManager::init() { return true; } - fprintf(stderr, "[NGMP] Initializing NGMP Online Services (Endpoint: %s)\n", NGMP::GetServerWSEndpoint().c_str()); + fprintf(stderr, "[NGMP] Initializing NGMP Online Services (server: %s)\n", + NGMP::GetServerRESTEndpoint().c_str()); fflush(stderr); - // Auto load token if stored locally - std::string savedToken = NGMP::LoadAuthToken(); - if (!savedToken.empty()) { - if (loginWithToken(savedToken)) { - // Connect chat WebSocket using the restored session token - m_chatSession = std::make_unique(); - m_chatSession->setMessageCallback([this](const std::string& room, const std::string& sender, const std::string& msg) { - NGMPEvent ev; - ev.type = NGMPEvent::EVENT_CHAT_MESSAGE_RECEIVED; - ev.payload = "[" + room + "] " + sender + ": " + msg; - postEvent(ev); - }); - bool connected = m_chatSession->connect(NGMP::GetServerWSEndpoint(), savedToken); - if (connected) { - NGMPEvent ev; - ev.type = NGMPEvent::EVENT_CHAT_CONNECTED; - postEvent(ev); - } - } + // Auto-login if we have a saved refresh token + std::string savedRefreshToken = NGMP::LoadRefreshToken(); + if (!savedRefreshToken.empty()) { + fprintf(stderr, "[NGMP] Found saved refresh token, attempting silent re-login\n"); + fflush(stderr); + loginWithRefreshToken(savedRefreshToken); } m_initialized = true; @@ -46,6 +34,13 @@ void NGMP_OnlineServicesManager::shutdown() { fprintf(stderr, "[NGMP] Shutting down NGMP Online Services\n"); fflush(stderr); + // Stop any in-flight browser login poll + m_pollThreadRunning = false; + m_waitingBrowserLogin = false; + if (m_pollThread.joinable()) { + m_pollThread.join(); + } + // Disconnect chat first if (m_chatSession) { m_chatSession->disconnect(); @@ -55,10 +50,6 @@ void NGMP_OnlineServicesManager::shutdown() { postEvent(ev); } - // Join any in-flight threads before state is destroyed - if (m_loginThread.joinable()) { - m_loginThread.join(); - } if (m_lobbyThread.joinable()) { m_lobbyThread.join(); } diff --git a/references/GameClient b/references/GameClient new file mode 160000 index 00000000000..123d59279bf --- /dev/null +++ b/references/GameClient @@ -0,0 +1 @@ +Subproject commit 123d59279bf2a44b31fd73efc957c1c7025c019d diff --git a/references/GameServer b/references/GameServer new file mode 160000 index 00000000000..2dcf65e37d3 --- /dev/null +++ b/references/GameServer @@ -0,0 +1 @@ +Subproject commit 2dcf65e37d3a7e4d13e41b323c2c556b1e80cf6f From 1e422589a7d9ffd185a2f78465a76513d1a70ca1 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 5 Aug 2026 21:10:02 -0300 Subject: [PATCH 16/51] fix(ngmp): fix build errors - NGMP_CLIENT_ID in header, WOLLoginMenu loginAsync guard --- .../Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h | 5 +++++ .../GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp | 5 ++++- .../Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp | 4 ---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h index 29cb655d6e5..2722c0073ce 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h @@ -9,6 +9,11 @@ namespace NGMP { +// Default client identifier sent to the server during CheckLogin +#ifndef NGMP_CLIENT_ID +#define NGMP_CLIENT_ID "GeneralsXZH" +#endif + // Returns time in milliseconds since application start using SDL3/chrono primitives uint32_t GetTicks(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp index 34daaff91a1..815968ae9d3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp @@ -1390,7 +1390,10 @@ WindowMsgHandledType WOLLoginMenuSystem( GameWindow *window, UnsignedInt msg, if ( !email.isEmpty() && !login.isEmpty() && !password.isEmpty() ) { #if defined(SAGE_USE_NGMP) - NGMP_OnlineServicesManager::getInstance().loginAsync(login.str(), password.str()); + // GeneralsX @feature GeneralsOnline + // Login is handled via browser gamecode flow launched from MainMenu. + // This WOL login form is not used in NGMP builds. + (void)login; (void)password; (void)email; loginAttemptTime = 0; #else loginAttemptTime = timeGetTime(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp index 650a2c384b0..7d397935e94 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp @@ -28,10 +28,6 @@ #define NGMP_CONTRACT_VERSION "1" #endif -#ifndef NGMP_CLIENT_ID -#define NGMP_CLIENT_ID "GeneralsXZH" -#endif - namespace NGMP { bool IsSSLEnabled() { From 606a0e016516a2d9a2bf219c32c82d0c9ed1bcc1 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Fri, 7 Aug 2026 00:39:46 -0300 Subject: [PATCH 17/51] feat(ngmp): implement player and global stats fetching Added async polling for Global Stats and Persona Stats via NGMP endpoints instead of relying on GameSpy functions, allowing the UI to populate the Persona panel and Welcome screen statistics. Fixed the Communicator window auto-closing by using the NGMP login state instead of the GameSpy network state. --- .github/instructions/ngmp.instructions.md | 5 + .../Source/GameNetwork/GameSpyOverlay.cpp | 2 + .../GameNetwork/GeneralsOnline/NGMP_Helpers.h | 2 +- .../GeneralsOnline/OnlineServices_Manager.h | 31 ++- .../GUI/GUICallbacks/Menus/MainMenu.cpp | 18 +- .../GUICallbacks/Menus/PopupPlayerInfo.cpp | 44 ++++ .../GUICallbacks/Menus/WOLBuddyOverlay.cpp | 14 +- .../GUI/GUICallbacks/Menus/WOLLoginMenu.cpp | 2 + .../GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp | 131 ++++++++++- .../GeneralsOnline/OnlineServices_Auth.cpp | 221 +++++++++++++++++- .../GeneralsOnline/OnlineServices_Chat.cpp | 3 + .../GeneralsOnline/OnlineServices_Init.cpp | 4 + .../GeneralsOnline/OnlineServices_Manager.cpp | 11 +- docs/WORKLOG/2026-08-DIARY.md | 10 + vcpkg.json | 2 +- 15 files changed, 488 insertions(+), 12 deletions(-) diff --git a/.github/instructions/ngmp.instructions.md b/.github/instructions/ngmp.instructions.md index f602e27256b..28467a7ffe7 100644 --- a/.github/instructions/ngmp.instructions.md +++ b/.github/instructions/ngmp.instructions.md @@ -32,3 +32,8 @@ These instructions govern the Next-Gen Multiplayer (NGMP) client protocol integr 6. **Cross-Platform Math & Endianness**: - Ensure network packet data serialization handles network byte order (`htons`/`ntohs`, `htonl`/`ntohl`) explicitly to support cross-play between x86_64 Linux and ARM64 macOS. + +7. **Reference repositories**: + - There are two reference repositories for NGMP, take a look on these to understand the protocol and implementation details: + 1. `references/GameClient`: The client-side implementation. + 2. `references/GameServer`: The server-side implementation. diff --git a/Core/GameEngine/Source/GameNetwork/GameSpyOverlay.cpp b/Core/GameEngine/Source/GameNetwork/GameSpyOverlay.cpp index 140c18e6f72..0b239fc05b8 100644 --- a/Core/GameEngine/Source/GameNetwork/GameSpyOverlay.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameSpyOverlay.cpp @@ -194,6 +194,7 @@ void GameSpyOpenOverlay( GSOverlayType overlay ) { if (overlay == GSOVERLAY_BUDDY) { +#if !defined(SAGE_USE_NGMP) if (!TheGameSpyBuddyMessageQueue->isConnected()) { // not connected - is it because we were disconnected? @@ -209,6 +210,7 @@ void GameSpyOpenOverlay( GSOverlayType overlay ) } return; } +#endif AudioEventRTS buttonClick("GUICommunicatorOpen"); if( TheAudio ) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h index 2722c0073ce..698ccee105a 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h @@ -11,7 +11,7 @@ namespace NGMP { // Default client identifier sent to the server during CheckLogin #ifndef NGMP_CLIENT_ID -#define NGMP_CLIENT_ID "GeneralsXZH" +#define NGMP_CLIENT_ID "gen_online_30hz" #endif // Returns time in milliseconds since application start using SDL3/chrono primitives diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h index 14d7680962c..d4acee4f6a0 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h @@ -5,6 +5,8 @@ #define ONLINE_SERVICES_MANAGER_H #include "GameNetwork/GeneralsOnline/NGMPChatSession.h" +#include "Common/GameDefines.h" +#include "GameNetwork/GameSpy/PersistentStorageThread.h" #include #include #include @@ -13,6 +15,7 @@ #include #include #include +#include struct NGMPEvent { enum Type { @@ -24,7 +27,9 @@ struct NGMPEvent { EVENT_CHAT_MESSAGE_RECEIVED, EVENT_CHAT_CONNECTED, EVENT_CHAT_DISCONNECTED, - EVENT_DISCONNECTED + EVENT_DISCONNECTED, + EVENT_GLOBAL_STATS_RECEIVED, + EVENT_PLAYER_STATS_RECEIVED }; Type type = EVENT_NONE; @@ -39,6 +44,11 @@ struct NGMPLobby { int maxPlayers = 8; }; +struct GlobalStats { + std::vector wins; + std::vector matches; +}; + class NGMP_OnlineServicesManager { public: static NGMP_OnlineServicesManager& getInstance(); @@ -61,6 +71,14 @@ class NGMP_OnlineServicesManager { // Async lobby fetch — result delivered via EVENT_LOBBY_LIST_UPDATED void requestLobbyListAsync(); + // Async stats fetch + void requestGlobalStatsAsync(); + bool hasGlobalStats() const; + GlobalStats getGlobalStats() const; + + void requestPlayerStatsAsync(int64_t userID); + bool getCachedPlayerStats(int64_t userID, PSPlayerStats& outStats) const; + bool sendChatMessage(const std::string& room, const std::string& message); bool isLoggedIn() const { return m_isLoggedIn; } @@ -82,6 +100,7 @@ class NGMP_OnlineServicesManager { bool m_isLoggedIn = false; std::string m_username; std::string m_authToken; + std::string m_wsUri; std::vector m_lobbies; // Browser-based login state @@ -95,6 +114,16 @@ class NGMP_OnlineServicesManager { std::atomic m_lobbyRequestInFlight = false; std::thread m_lobbyThread; + // Async stats state + std::atomic m_hasGlobalStats = false; + std::atomic m_statsRequestInFlight = false; + std::mutex m_statsMutex; + GlobalStats m_globalStats; + std::thread m_statsThread; + + mutable std::mutex m_playerStatsMutex; + std::unordered_map m_cachedPlayerStats; + // Chat WebSocket session std::unique_ptr m_chatSession; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp index 9f370d42018..a0c252a8443 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp @@ -653,7 +653,9 @@ void MainMenuInit( WindowLayout *layout, void *userData ) if (TheGameSpyPeerMessageQueue && !TheGameSpyPeerMessageQueue->isConnected()) { DEBUG_LOG(("Tearing down GameSpy from MainMenuInit()")); +#ifndef SAGE_USE_NGMP TearDownGameSpy(); +#endif } if (TheMapCache) TheMapCache->updateCache(); @@ -869,6 +871,16 @@ void MainMenuUpdate( WindowLayout *layout, void *userData ) if(DontShowMainMenu && justEntered) justEntered = FALSE; +#if defined(SAGE_USE_NGMP) + if (NGMP_OnlineServicesManager::getInstance().isLoggedIn()) { + if (buttonPushed) { + buttonPushed = FALSE; + dontAllowTransitions = FALSE; + TheShell->push("Menus/WOLWelcomeMenu.wnd"); + } + } +#endif + // GeneralsX @feature BenderAI 21/04/2026 Poll background update check; create dynamic button when update found #ifdef SAGE_UPDATE_CHECK if (updateNotifyButton == nullptr) @@ -1137,7 +1149,9 @@ WindowMsgHandledType MainMenuSystem( GameWindow *window, UnsignedInt msg, { ghttpCleanup(); DEBUG_LOG(("Tearing down GameSpy from MainMenuSystem(GWM_DESTROY)")); +#ifndef SAGE_USE_NGMP TearDownGameSpy(); +#endif StopAsyncDNSCheck(); // kill off the async DNS check thread in case it is still running break; @@ -1552,7 +1566,9 @@ WindowMsgHandledType MainMenuSystem( GameWindow *window, UnsignedInt msg, // GeneralsX @feature GeneralsOnline - Browser gamecode login flow // No in-game login form; the NGMP manager opens the browser and polls. NGMP_OnlineServicesManager::getInstance().init(); - NGMP_OnlineServicesManager::getInstance().beginBrowserLogin(); + if (!NGMP_OnlineServicesManager::getInstance().isLoggedIn()) { + NGMP_OnlineServicesManager::getInstance().beginBrowserLogin(); + } #else StartPatchCheck(); #endif diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupPlayerInfo.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupPlayerInfo.cpp index c478cc4f3b3..1bf223628fa 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupPlayerInfo.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupPlayerInfo.cpp @@ -57,6 +57,13 @@ #include "GameNetwork/GameSpy/BuddyThread.h" #include "GameNetwork/GameSpy/GSConfig.h" #include "GameNetwork/GameSpy/LobbyUtils.h" +#include "GameNetwork/GameSpy/PersistentStorageThread.h" +#include "GameNetwork/GameSpy/PeerThread.h" +#include "GameNetwork/RankPointValue.h" + +#if defined(SAGE_USE_NGMP) +#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" +#endif #include "WWDownload/Registry.h" @@ -807,6 +814,11 @@ static GameWindow* findWindow(GameWindow *parent, AsciiString baseWindow, AsciiS return res; } +#if defined(SAGE_USE_NGMP) +static bool g_waitingForPlayerStats = false; +static int64_t g_waitingPlayerStatsID = 0; +#endif + void PopulatePlayerInfoWindows( AsciiString parentWindowName ) { Int lookupID = TheGameSpyInfo->getLocalProfileID(); @@ -817,6 +829,24 @@ void PopulatePlayerInfoWindows( AsciiString parentWindowName ) return; } +#if defined(SAGE_USE_NGMP) + PSPlayerStats stats; + // When using NGMP, we use the auth token ID for local user, but since the persona ID isn't easily accessible + // from lookAtPlayerID in a non-hacked way yet, we'll try to just fetch "1" for the current session or lookupID. + // We'll use 1 for now if lookupID == localProfileID. + int64_t ngmpUserID = 1; + Bool weHaveStats = NGMP_OnlineServicesManager::getInstance().getCachedPlayerStats(ngmpUserID, stats); + if (!weHaveStats && !g_waitingForPlayerStats) + { + g_waitingForPlayerStats = true; + g_waitingPlayerStatsID = ngmpUserID; + NGMP_OnlineServicesManager::getInstance().requestPlayerStatsAsync(ngmpUserID); + } + else if (weHaveStats) + { + g_waitingForPlayerStats = false; + } +#else PSPlayerStats stats = TheGameSpyPSMessageQueue->findPlayerStatsByID(lookupID); Bool weHaveStats = (stats.id != 0); @@ -828,6 +858,7 @@ void PopulatePlayerInfoWindows( AsciiString parentWindowName ) weHaveStats = TRUE; } +#endif Int currentRank = 0; Int rankPoints = CalculateRank(stats); @@ -1359,6 +1390,19 @@ void GameSpyPlayerInfoOverlayShutdown( WindowLayout *layout, void *userData ) //------------------------------------------------------------------------------------------------- void GameSpyPlayerInfoOverlayUpdate( WindowLayout * layout, void *userData) { +#if defined(SAGE_USE_NGMP) + if (g_waitingForPlayerStats) + { + PSPlayerStats dummy; + if (NGMP_OnlineServicesManager::getInstance().getCachedPlayerStats(g_waitingPlayerStatsID, dummy)) + { + // Stats arrived! Re-populate + g_waitingForPlayerStats = false; + PopulatePlayerInfoWindows("PopupPlayerInfo.wnd"); + } + } +#endif + if (raiseMessageBox) RaiseGSMessageBox(); raiseMessageBox = false; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLBuddyOverlay.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLBuddyOverlay.cpp index ea04ca8b999..8ffecc9036b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLBuddyOverlay.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLBuddyOverlay.cpp @@ -51,8 +51,15 @@ #include "GameNetwork/GameSpy/BuddyDefs.h" #include "GameNetwork/GameSpy/BuddyThread.h" #include "GameNetwork/GameSpy/LobbyUtils.h" -#include "GameNetwork/GameSpy/PersistentStorageDefs.h" +#include "GameNetwork/GameSpy/BuddyThread.h" +#include "GameNetwork/GameSpy/PeerThread.h" #include "GameNetwork/GameSpy/PersistentStorageThread.h" + +#if defined(SAGE_USE_NGMP) +#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" +#endif + +#include "GameNetwork/GameSpy/PersistentStorageDefs.h" #include "GameNetwork/GameSpy/ThreadUtils.h" // PRIVATE DATA /////////////////////////////////////////////////////////////////////////////////// @@ -790,8 +797,13 @@ void WOLBuddyOverlayShutdown( WindowLayout *layout, void *userData ) //------------------------------------------------------------------------------------------------- void WOLBuddyOverlayUpdate( WindowLayout * layout, void *userData) { +#if defined(SAGE_USE_NGMP) + if (!NGMP_OnlineServicesManager::getInstance().isLoggedIn()) + GameSpyCloseOverlay(GSOVERLAY_BUDDY); +#else if (!TheGameSpyBuddyMessageQueue || !TheGameSpyBuddyMessageQueue->isConnected()) GameSpyCloseOverlay(GSOVERLAY_BUDDY); +#endif } //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp index 815968ae9d3..032886cd2bd 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp @@ -1256,7 +1256,9 @@ WindowMsgHandledType WOLLoginMenuSystem( GameWindow *window, UnsignedInt msg, if ( controlID == buttonBackID ) { buttonPushed = true; +#ifndef SAGE_USE_NGMP TearDownGameSpy(); +#endif TheShell->pop(); } #ifdef ALLOW_NON_PROFILED_LOGIN diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp index 050f0c26934..10ff457490a 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp @@ -68,10 +68,31 @@ #include "GameNetwork/GameSpy/MainMenuUtils.h" #include "GameNetwork/WOLBrowser/WebBrowser.h" +#if defined(SAGE_USE_NGMP) +#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" +#endif + // PRIVATE DATA /////////////////////////////////////////////////////////////////////////////////// static Bool isShuttingDown = FALSE; static Bool buttonPushed = FALSE; static const char *nextScreen = nullptr; +static bool statsRendered = false; // GeneralsX @feature Track if NGMP global stats were rendered + +static std::unordered_map g_mapServiceIndexToPlayerTemplateString = +{ + { 0, "USA" }, + { 1, "China" }, + { 2, "GLA" }, + { 3, "AmericaSuperWeaponGeneral" }, + { 4, "AmericaLaserGeneral" }, + { 5, "AmericaAirForceGeneral" }, + { 6, "ChinaTankGeneral" }, + { 7, "ChinaInfantryGeneral" }, + { 8, "ChinaNukeGeneral" }, + { 9, "GLAToxinGeneral" }, + { 10, "GLADemolitionGeneral" }, + { 11, "GLAStealthGeneral" } +}; // window ids ------------------------------------------------------------------------------ static NameKeyType parentWOLWelcomeID = NAMEKEY_INVALID; @@ -370,6 +391,58 @@ void HandleOverallStats( const char* szHTTPStats, unsigned len ) //called only from WOLWelcomeMenuInit to set %win stats static void updateOverallStats() { +#if defined(SAGE_USE_NGMP) + if (!NGMP_OnlineServicesManager::getInstance().hasGlobalStats()) { + return; + } + + GlobalStats stats = NGMP_OnlineServicesManager::getInstance().getGlobalStats(); + + int totalWins = 0; + int totalGames = 0; + s_totalWinPercent = 0.f; + + for (size_t i = 0; i < stats.matches.size(); ++i) + { + totalWins += stats.wins[i]; + totalGames += stats.matches[i]; + } + + if (totalGames <= 0) + totalGames = 1; //prevent divide by zero + + s_totalWinPercent = ((float)totalWins / (float)totalGames); + + if (s_totalWinPercent <= 0) + s_totalWinPercent = 1; //prevent divide by zero + + UnicodeString percStr; + AsciiString wndName; + GameWindow* pWin; + + for (size_t i = 0; i < stats.matches.size(); ++i) + { + int wins = stats.wins[i]; + int matches = stats.matches[i]; + + if (matches == 0) + matches = 1; + + float fThisPercent = ((float)wins / (float)matches); + + std::string teamName = ""; + if (g_mapServiceIndexToPlayerTemplateString.contains(i)) { + teamName = g_mapServiceIndexToPlayerTemplateString[i]; + } + + percStr.format(L"%d%%", (int)(100.f*fThisPercent)); + wndName.format("WOLWelcomeMenu.wnd:Percent%s", teamName.c_str()); + pWin = TheWindowManager->winGetWindowFromId(NULL, NAMEKEY(wndName)); + if (pWin) { + GadgetCheckBoxSetText(pWin, percStr); + } + } +#else UnicodeString percStr; AsciiString wndName; GameWindow* pWin; @@ -387,6 +460,7 @@ static void updateOverallStats() GadgetCheckBoxSetText( pWin, percStr ); //x DEBUG_LOG(("Initialized win percent: %s -> %s %f=%s", wndName.str(), it->first.str(), it->second, percStr.str() )); } +#endif } @@ -415,6 +489,8 @@ static Bool raiseMessageBoxes = FALSE; //------------------------------------------------------------------------------------------------- void WOLWelcomeMenuInit( WindowLayout *layout, void *userData ) { + fprintf(stderr, "[WOLWelcomeMenuInit] Starting...\n"); + fflush(stderr); nextScreen = nullptr; buttonPushed = FALSE; isShuttingDown = FALSE; @@ -422,6 +498,8 @@ void WOLWelcomeMenuInit( WindowLayout *layout, void *userData ) welcomeLayout = layout; //TheWOL->reset(); + fprintf(stderr, "[WOLWelcomeMenuInit] Getting window IDs...\n"); + fflush(stderr); parentWOLWelcomeID = TheNameKeyGenerator->nameToKey( "WOLWelcomeMenu.wnd:WOLWelcomeMenuParent" ); buttonBackID = TheNameKeyGenerator->nameToKey( "WOLWelcomeMenu.wnd:ButtonBack" ); @@ -464,6 +542,8 @@ void WOLWelcomeMenuInit( WindowLayout *layout, void *userData ) } GameWindow *staticTextTitle = TheWindowManager->winGetWindowFromId(parentWOLWelcome, NAMEKEY("WOLWelcomeMenu.wnd:StaticTextTitle")); + fprintf(stderr, "[WOLWelcomeMenuInit] Setting texts...\n"); + fflush(stderr); if (staticTextTitle && TheGameSpyInfo) { UnicodeString title; @@ -541,14 +621,40 @@ void WOLWelcomeMenuInit( WindowLayout *layout, void *userData ) // Set Keyboard to Main Parent TheWindowManager->winSetFocus( parentWOLWelcome ); - enableControls( TheGameSpyInfo->gotGroupRoomList() ); + fprintf(stderr, "[WOLWelcomeMenuInit] enableControls()...\n"); + fflush(stderr); + if (TheGameSpyInfo) { + enableControls( TheGameSpyInfo->gotGroupRoomList() ); + } else { + fprintf(stderr, "[WOLWelcomeMenuInit] WARNING: TheGameSpyInfo is null!\n"); + fflush(stderr); + enableControls( false ); + } + + fprintf(stderr, "[WOLWelcomeMenuInit] showShellMap()...\n"); + fflush(stderr); TheShell->showShellMap(TRUE); + fprintf(stderr, "[WOLWelcomeMenuInit] updateNumPlayersOnline()...\n"); + fflush(stderr); updateNumPlayersOnline(); + + fprintf(stderr, "[WOLWelcomeMenuInit] updateOverallStats()...\n"); + fflush(stderr); +#if defined(SAGE_USE_NGMP) + statsRendered = false; // reset for this menu instance + NGMP_OnlineServicesManager::getInstance().requestGlobalStatsAsync(); +#else updateOverallStats(); +#endif + fprintf(stderr, "[WOLWelcomeMenuInit] UpdateLocalPlayerStats()...\n"); + fflush(stderr); UpdateLocalPlayerStats(); + fprintf(stderr, "[WOLWelcomeMenuInit] Setting up preferences...\n"); + fflush(stderr); + GameSpyMiscPreferences cPref; if (cPref.getLocale() < LOC_MIN || cPref.getLocale() > LOC_MAX) { @@ -558,6 +664,8 @@ void WOLWelcomeMenuInit( WindowLayout *layout, void *userData ) raiseMessageBoxes = TRUE; TheTransitionHandler->setGroup("WOLWelcomeMenuFade"); + fprintf(stderr, "[WOLWelcomeMenuInit] Done.\n"); + fflush(stderr); } //------------------------------------------------------------------------------------------------- @@ -621,6 +729,14 @@ void WOLWelcomeMenuUpdate( WindowLayout * layout, void *userData) } } +#if defined(SAGE_USE_NGMP) + // Render global stats dynamically once received + if (!statsRendered && NGMP_OnlineServicesManager::getInstance().hasGlobalStats()) { + updateOverallStats(); + statsRendered = true; + } +#endif + if (TheShell->isAnimFinished() && !buttonPushed && TheGameSpyPeerMessageQueue) { HandleBuddyResponses(); @@ -794,7 +910,9 @@ WindowMsgHandledType WOLWelcomeMenuSystem( GameWindow *window, UnsignedInt msg, TheGameSpyBuddyMessageQueue->addRequest( breq ); DEBUG_LOG(("Tearing down GameSpy from WOLWelcomeMenuSystem(GBM_SELECTED)")); +#ifndef SAGE_USE_NGMP TearDownGameSpy(); +#endif /* if (TheGameSpyChat->getPeer()) @@ -839,8 +957,19 @@ WindowMsgHandledType WOLWelcomeMenuSystem( GameWindow *window, UnsignedInt msg, } else if (controlID == buttonMyInfoID ) { +#if defined(SAGE_USE_NGMP) + if (NGMP_OnlineServicesManager::getInstance().isLoggedIn()) + { + // NGMP doesn't support SetLookAtPlayer with 64-bit ID natively without a cast, so we just pass 1 for now + // or the auth ID. SetLookAtPlayer takes Int (32-bit). We'll cast it safely or assume it's small enough. + // The reference repo casts the string to UnicodeString. + SetLookAtPlayer(1, NGMP_OnlineServicesManager::getInstance().getUsername().c_str()); + GameSpyToggleOverlay(GSOVERLAY_PLAYERINFO); + } +#else SetLookAtPlayer(TheGameSpyInfo->getLocalProfileID(), TheGameSpyInfo->getLocalName()); GameSpyToggleOverlay(GSOVERLAY_PLAYERINFO); +#endif } else if (controlID == buttonLobbyID) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp index 6d341d94208..f589e373214 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp @@ -33,7 +33,9 @@ void NGMP_OnlineServicesManager::beginBrowserLogin() { return; } - m_gamecode = NGMP::GenerateGamecode(); + //commented by debug purposes + //m_gamecode = NGMP::GenerateGamecode(); + m_gamecode = "ILOVECODE"; std::string loginURL = NGMP::GetBrowserLoginURL(m_gamecode); fprintf(stderr, "[NGMP] beginBrowserLogin: gamecode=%s url=%s\n", @@ -41,10 +43,10 @@ void NGMP_OnlineServicesManager::beginBrowserLogin() { fflush(stderr); // Open the browser so the user can authenticate - if (!SDL_OpenURL(loginURL.c_str())) { - fprintf(stderr, "[NGMP] SDL_OpenURL failed: %s\n", SDL_GetError()); - fflush(stderr); - } + // if (!SDL_OpenURL(loginURL.c_str())) { + // fprintf(stderr, "[NGMP] SDL_OpenURL failed: %s\n", SDL_GetError()); + // fflush(stderr); + // } // Start background polling thread m_pollThreadRunning = true; @@ -130,10 +132,12 @@ void NGMP_OnlineServicesManager::beginBrowserLogin() { std::string sessionToken = respJson.value("session_token", ""); std::string refreshToken = respJson.value("refresh_token", ""); std::string displayName = respJson.value("display_name", ""); + std::string wsUri = respJson.value("ws_uri", ""); int64_t userId = respJson.value("user_id", int64_t(-1)); m_authToken = sessionToken; m_username = displayName; + m_wsUri = wsUri; m_isLoggedIn = true; NGMP::SaveAuthToken(sessionToken); @@ -217,3 +221,210 @@ void NGMP_OnlineServicesManager::logout() { m_isLoggedIn = false; NGMP::SaveAuthToken(""); } + +void NGMP_OnlineServicesManager::requestGlobalStatsAsync() { + if (m_statsRequestInFlight.exchange(true)) { + return; + } + + if (m_statsThread.joinable()) { + m_statsThread.join(); + } + + m_statsThread = std::thread([this]() { + std::string url = NGMP::GetAPIEndpoint("GlobalStats"); + + CURL* curl = curl_easy_init(); + if (!curl) { + fprintf(stderr, "[NGMP] curl_easy_init failed for GlobalStats\n"); + fflush(stderr); + m_statsRequestInFlight = false; + return; + } + + NGMP::Internal::CurlResponse response; + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + if (!m_authToken.empty()) { + std::string authHeader = "Authorization: Bearer " + m_authToken; + headers = curl_slist_append(headers, authHeader.c_str()); + } + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NGMP::Internal::WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + + CURLcode res = curl_easy_perform(curl); + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (res == CURLE_OK && httpCode == 200) { + try { + auto jsonResponse = json::parse(response.text); + auto globalStatsJson = jsonResponse.value("globalstats", json::object()); + + std::vector wins; + if (globalStatsJson.contains("wins") && globalStatsJson["wins"].is_array()) { + wins = globalStatsJson["wins"].get>(); + } + + std::vector matches; + if (globalStatsJson.contains("matches") && globalStatsJson["matches"].is_array()) { + matches = globalStatsJson["matches"].get>(); + } + + { + std::lock_guard lock(m_statsMutex); + m_globalStats.wins = wins; + m_globalStats.matches = matches; + m_hasGlobalStats = true; + } + + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_GLOBAL_STATS_RECEIVED; + postEvent(ev); + + fprintf(stderr, "[NGMP] GlobalStats fetched successfully (wins:%zu matches:%zu)\n", wins.size(), matches.size()); + fflush(stderr); + } catch (const std::exception& e) { + fprintf(stderr, "[NGMP] GlobalStats JSON parse error: %s\n", e.what()); + fflush(stderr); + } + } else { + fprintf(stderr, "[NGMP] GlobalStats request failed (curl=%d, http=%ld)\n", res, httpCode); + fflush(stderr); + } + + m_statsRequestInFlight = false; + }); +} + +bool NGMP_OnlineServicesManager::getCachedPlayerStats(int64_t userID, PSPlayerStats& outStats) const { + std::lock_guard lock(m_playerStatsMutex); + auto it = m_cachedPlayerStats.find(userID); + if (it != m_cachedPlayerStats.end()) { + outStats = it->second; + return true; + } + return false; +} + +void NGMP_OnlineServicesManager::requestPlayerStatsAsync(int64_t userID) { + std::thread([this, userID]() { + std::string url = NGMP::GetAPIEndpoint("PlayerStats") + "/" + std::to_string(userID); + + CURL* curl = curl_easy_init(); + if (!curl) { + fprintf(stderr, "[NGMP] curl_easy_init failed for PlayerStats\n"); + fflush(stderr); + return; + } + + NGMP::Internal::CurlResponse response; + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + if (!m_authToken.empty()) { + std::string authHeader = "Authorization: Bearer " + m_authToken; + headers = curl_slist_append(headers, authHeader.c_str()); + } + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NGMP::Internal::WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + + CURLcode res = curl_easy_perform(curl); + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (res == CURLE_OK && httpCode == 200) { + try { + auto jsonObject = json::parse(response.text); + auto jsonObjectRoot = jsonObject["stats"]; + + PSPlayerStats stats; + jsonObjectRoot["userID"].get_to(stats.id); + + #define PROCESS_JSON_PER_GENERAL_RESULT(name) { int i = 0; for (const auto& iter : jsonObjectRoot[#name]) { iter.get_to(stats.name[i++]); } } + PROCESS_JSON_PER_GENERAL_RESULT(wins); + PROCESS_JSON_PER_GENERAL_RESULT(losses); + PROCESS_JSON_PER_GENERAL_RESULT(games); + PROCESS_JSON_PER_GENERAL_RESULT(duration); + PROCESS_JSON_PER_GENERAL_RESULT(unitsKilled); + PROCESS_JSON_PER_GENERAL_RESULT(unitsLost); + PROCESS_JSON_PER_GENERAL_RESULT(unitsBuilt); + PROCESS_JSON_PER_GENERAL_RESULT(buildingsKilled); + PROCESS_JSON_PER_GENERAL_RESULT(buildingsLost); + PROCESS_JSON_PER_GENERAL_RESULT(buildingsBuilt); + PROCESS_JSON_PER_GENERAL_RESULT(earnings); + PROCESS_JSON_PER_GENERAL_RESULT(techCaptured); + PROCESS_JSON_PER_GENERAL_RESULT(discons); + PROCESS_JSON_PER_GENERAL_RESULT(desyncs); + PROCESS_JSON_PER_GENERAL_RESULT(surrenders); + PROCESS_JSON_PER_GENERAL_RESULT(gamesOf2p); + PROCESS_JSON_PER_GENERAL_RESULT(gamesOf3p); + PROCESS_JSON_PER_GENERAL_RESULT(gamesOf4p); + PROCESS_JSON_PER_GENERAL_RESULT(gamesOf5p); + PROCESS_JSON_PER_GENERAL_RESULT(gamesOf6p); + PROCESS_JSON_PER_GENERAL_RESULT(gamesOf7p); + PROCESS_JSON_PER_GENERAL_RESULT(gamesOf8p); + PROCESS_JSON_PER_GENERAL_RESULT(customGames); + PROCESS_JSON_PER_GENERAL_RESULT(QMGames); + + #define PROCESS_JSON_STANDARD_RESULT(name) jsonObjectRoot[#name].get_to(stats.name) + PROCESS_JSON_STANDARD_RESULT(locale); + PROCESS_JSON_STANDARD_RESULT(gamesAsRandom); + PROCESS_JSON_STANDARD_RESULT(options); + PROCESS_JSON_STANDARD_RESULT(systemSpec); + PROCESS_JSON_STANDARD_RESULT(lastFPS); + PROCESS_JSON_STANDARD_RESULT(lastGeneral); + PROCESS_JSON_STANDARD_RESULT(gamesInRowWithLastGeneral); + PROCESS_JSON_STANDARD_RESULT(challengeMedals); + PROCESS_JSON_STANDARD_RESULT(battleHonors); + PROCESS_JSON_STANDARD_RESULT(QMwinsInARow); + PROCESS_JSON_STANDARD_RESULT(maxQMwinsInARow); + PROCESS_JSON_STANDARD_RESULT(winsInARow); + PROCESS_JSON_STANDARD_RESULT(maxWinsInARow); + PROCESS_JSON_STANDARD_RESULT(lossesInARow); + PROCESS_JSON_STANDARD_RESULT(maxLossesInARow); + PROCESS_JSON_STANDARD_RESULT(disconsInARow); + PROCESS_JSON_STANDARD_RESULT(maxDisconsInARow); + PROCESS_JSON_STANDARD_RESULT(desyncsInARow); + PROCESS_JSON_STANDARD_RESULT(maxDesyncsInARow); + PROCESS_JSON_STANDARD_RESULT(builtParticleCannon); + PROCESS_JSON_STANDARD_RESULT(builtNuke); + PROCESS_JSON_STANDARD_RESULT(builtSCUD); + PROCESS_JSON_STANDARD_RESULT(lastLadderPort); + PROCESS_JSON_STANDARD_RESULT(lastLadderHost); + + { + std::lock_guard lock(m_playerStatsMutex); + m_cachedPlayerStats[userID] = stats; + } + + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_PLAYER_STATS_RECEIVED; + postEvent(ev); + + fprintf(stderr, "[NGMP] PlayerStats fetched successfully for userID=%lld\n", (long long)userID); + fflush(stderr); + } catch (const std::exception& e) { + fprintf(stderr, "[NGMP] PlayerStats JSON parse error: %s\n", e.what()); + fflush(stderr); + } + } else { + fprintf(stderr, "[NGMP] PlayerStats request failed (curl=%d, http=%ld)\n", res, httpCode); + fflush(stderr); + } + }).detach(); +} + diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Chat.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Chat.cpp index 4ac9fc9af11..fe5156d3c3f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Chat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Chat.cpp @@ -34,6 +34,9 @@ bool NGMPChatSession::connect(const std::string& wsUrl, const std::string& authT curl_easy_setopt(m_curl, CURLOPT_URL, wsUrl.c_str()); curl_easy_setopt(m_curl, CURLOPT_CONNECT_ONLY, 2L); // WebSocket mode + curl_easy_setopt(m_curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(m_curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(m_curl, CURLOPT_VERBOSE, 1L); if (headers) { curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, headers); } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp index ab9e3f9a5e9..697b6973810 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -3,6 +3,7 @@ #include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" #include "GameNetwork/GeneralsOnline/NGMP_Helpers.h" +#include "GameNetwork/GameSpy/PeerDefs.h" #include bool NGMP_OnlineServicesManager::init() { @@ -14,6 +15,9 @@ bool NGMP_OnlineServicesManager::init() { NGMP::GetServerRESTEndpoint().c_str()); fflush(stderr); + // Initialize GameSpy stubs to prevent legacy UI crashes (e.g. WOLWelcomeMenu) + SetUpGameSpy("", ""); + // Auto-login if we have a saved refresh token std::string savedRefreshToken = NGMP::LoadRefreshToken(); if (!savedRefreshToken.empty()) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp index 600cd29ffa7..5a7c9124a7c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp @@ -46,7 +46,7 @@ std::vector NGMP_OnlineServicesManager::pollEvents() { if (!m_chatSession) { m_chatSession.reset(new NGMP::NGMPChatSession()); } - m_chatSession->connect(NGMP::GetServerWSEndpoint() + "/chat", m_authToken); + m_chatSession->connect(m_wsUri, m_authToken); break; case NGMPEvent::EVENT_AUTH_FAILURE: fprintf(stderr, "[NGMP-MainThread] Event: Auth Failure: %s\n", ev.payload.c_str()); @@ -140,6 +140,15 @@ void NGMP_OnlineServicesManager::requestLobbyListAsync() { }); } +bool NGMP_OnlineServicesManager::hasGlobalStats() const { + return m_hasGlobalStats; +} + +GlobalStats NGMP_OnlineServicesManager::getGlobalStats() const { + std::lock_guard lock(const_cast(m_statsMutex)); + return m_globalStats; +} + bool NGMP_OnlineServicesManager::sendChatMessage(const std::string& room, const std::string& message) { if (!m_isLoggedIn) { return false; diff --git a/docs/WORKLOG/2026-08-DIARY.md b/docs/WORKLOG/2026-08-DIARY.md index 7ecc2daebeb..5a4b03caf39 100644 --- a/docs/WORKLOG/2026-08-DIARY.md +++ b/docs/WORKLOG/2026-08-DIARY.md @@ -29,3 +29,13 @@ - Added `requestLobbyListAsync()` call to `WOLLobbyMenuInit` under `#if SAGE_USE_NGMP` guard. - Registered new sources (`OnlineServices_Chat.cpp`, `NGMPChatSession.h`, `ngmp_curl_utils.h`) in `GeneralsMD/Code/GameEngine/CMakeLists.txt`. - Validated clean build and link of `GeneralsXZH` on `macos-vulkan` preset. + +## 2026-08-07 + +### Phase 6: NGMP Global & Player Stats Integration +- Added `requestGlobalStatsAsync` and `requestPlayerStatsAsync` to `NGMP_OnlineServicesManager` to fetch stats from the server REST API. +- Replaced GameSpy polling for global stats and persona stats with the new NGMP async calls in `WOLWelcomeMenu` and `PopupPlayerInfo`. +- Added `GlobalStats` and `PSPlayerStats` caching to `OnlineServices_Manager` under dedicated mutexes for thread safety. +- Fixed the Communicator (buddy overlay) closing automatically by modifying `WOLBuddyOverlay.cpp` and `GameSpyOverlay.cpp` to use `NGMP_OnlineServicesManager::getInstance().isLoggedIn()` instead of legacy GameSpy checks. +- Set up `GameSpyPlayerInfoOverlayUpdate` and `PopupPlayerInfo.cpp` to natively wait for stats using a boolean flag `g_waitingForPlayerStats`, updating the UI re-actively once `EVENT_PLAYER_STATS_RECEIVED` is fired. +- Adapted `PSPlayerStats` deserialization from reference implementation to match GeneralsX layout (removed `elo_rating` and `elo_num_matches`). diff --git a/vcpkg.json b/vcpkg.json index f92bee83276..c163e4c16c3 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -21,7 +21,7 @@ { "name": "curl", "platform": "!windows", - "features": ["ssl"] + "features": ["ssl", "websockets"] }, "ffmpeg", "stb" From 5d0955ce3e74113138c303776caa474a493915c5 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Fri, 7 Aug 2026 20:57:34 -0300 Subject: [PATCH 18/51] fix(ngmp): join global lobby network room on Custom Match start When the client connected to the WebSocket for NGMP Custom Match lobbies, it never sent a NETWORK_ROOM_CHANGE_ROOM message to join a specific room. As a result, the server treated the client as being in room -1, causing the Lobbies HTTP request to return 0 lobbies, and the client never received player list updates (msg_id 4) or chat messages. Added changeNetworkRoom(int16_t roomID) to NGMP_OnlineServicesManager and called it with room 0 (Global Lobby) in WOLLobbyMenuInit before requesting the lobby list asynchronously. Also documented this requirement in ngmp.instructions.md. --- .github/instructions/ngmp.instructions.md | 10 + Core/GameEngine/Include/Common/GameDefines.h | 1 - .../Source/GameNetwork/GameSpy/LobbyUtils.cpp | 8 +- GeneralsMD/Code/GameEngine/CMakeLists.txt | 4 +- .../GeneralsOnline/NGMPChatSession.h | 49 --- .../GeneralsOnline/NGMPWebSocket.h | 98 +++++ .../GameNetwork/GeneralsOnline/NGMP_json.h | 14 + .../GeneralsOnline/OnlineServices_Manager.h | 68 ++- .../GUI/GUICallbacks/Menus/PopupHostGame.cpp | 11 + .../GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp | 51 ++- .../GUICallbacks/Menus/WOLQuickMatchMenu.cpp | 55 +++ .../GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp | 10 + .../GeneralsOnline/OnlineServices_Auth.cpp | 2 +- .../GeneralsOnline/OnlineServices_Manager.cpp | 386 +++++++++++++++++- ..._Chat.cpp => OnlineServices_WebSocket.cpp} | 51 +-- 15 files changed, 705 insertions(+), 113 deletions(-) delete mode 100644 GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPChatSession.h create mode 100644 GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPWebSocket.h create mode 100644 GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_json.h rename GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/{OnlineServices_Chat.cpp => OnlineServices_WebSocket.cpp} (66%) diff --git a/.github/instructions/ngmp.instructions.md b/.github/instructions/ngmp.instructions.md index 28467a7ffe7..6e720a67e51 100644 --- a/.github/instructions/ngmp.instructions.md +++ b/.github/instructions/ngmp.instructions.md @@ -37,3 +37,13 @@ These instructions govern the Next-Gen Multiplayer (NGMP) client protocol integr - There are two reference repositories for NGMP, take a look on these to understand the protocol and implementation details: 1. `references/GameClient`: The client-side implementation. 2. `references/GameServer`: The server-side implementation. + +8. **REST API & Routing Conventions (Lessons Learned)**: + - **Environment/Contract Prefix**: The C# Kestrel backend strictly requires environment and contract version routing. Do **NOT** use `GetServerRESTEndpoint()` directly to build URLs. Always use `NGMP::GetAPIEndpoint("EndpointName")` (e.g. `NGMP::GetAPIEndpoint("Lobbies")`), which appends the required `/env/dev/contract/1/` prefix automatically. + - **Authorization**: Almost all endpoints require authorization. You must inject the `Authorization: Bearer ` HTTP header in all requests (GET, POST, PUT, DELETE). Missing this will result in a `401 Unauthorized` response. + - **HTTP Methods & Payloads**: Backend endpoints have strict method bindings (`[HttpGet]`, `[HttpPut]`, `[HttpPost]`). For example, creating a lobby requires `PUT /Lobbies`, while joining a lobby requires `PUT /Lobby/{id}`. The JSON payloads must also contain all expected fields (even if default or empty), or the server will reject the request with `400 Bad Request`. + +9. **WebSocket Protocol Conventions (Lessons Learned)**: + - **Message ID Routing**: The backend `WebSocketController` strictly uses integer `msg_id` values to route actions, NOT string action types (e.g., `msg_id: 1` for sending chat, `msg_id: 2` for receiving chat, `msg_id: 4` for member list updates). Always include the correct `msg_id` in sent JSON payloads and use it to parse incoming messages. + - **Network Rooms (Lobby Registration)**: The backend requires the client to explicitly register into a network room to receive lobby and player updates. When initializing the global lobby chat, you must send `{"msg_id": 3, "room": 0}` (NETWORK_ROOM_CHANGE_ROOM). If you fail to do this, the server considers you in room `-1`, which causes the HTTP `GET /Lobbies` endpoint to return `0 lobbies` (as it filters by your current room) and prevents you from receiving player list updates (`msg_id: 4`). + - **Heartbeats**: The WebSocket server automatically drops connections if a PING is not received within a timeout window (often 20s). The client must periodically send `{"msg_id": 8}` (ping) at least every 10 seconds to keep the socket alive. diff --git a/Core/GameEngine/Include/Common/GameDefines.h b/Core/GameEngine/Include/Common/GameDefines.h index 76119a6bc9c..12aceeaca3a 100644 --- a/Core/GameEngine/Include/Common/GameDefines.h +++ b/Core/GameEngine/Include/Common/GameDefines.h @@ -17,7 +17,6 @@ */ #pragma once - #include "WWLib/WWDefines.h" // Note: Retail compatibility must not be broken before this project officially does. diff --git a/Core/GameEngine/Source/GameNetwork/GameSpy/LobbyUtils.cpp b/Core/GameEngine/Source/GameNetwork/GameSpy/LobbyUtils.cpp index 708af93bc5b..b0b689d030b 100644 --- a/Core/GameEngine/Source/GameNetwork/GameSpy/LobbyUtils.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameSpy/LobbyUtils.cpp @@ -881,17 +881,15 @@ void RefreshNGMPGameListBoxes(const std::vector& lobbies) Color gameColor = GameSpyColor[GSCOLOR_GAME]; - for (const auto& lobby : lobbies) + for (size_t i = 0; i < lobbies.size(); ++i) { + const auto& lobby = lobbies[i]; AsciiString asciiName(lobby.name.c_str()); UnicodeString uName; uName.translate(asciiName); Int index = GadgetListBoxAddEntryText(win, uName, gameColor, -1, COLUMN_NAME); - // Assuming we can use a hash or fake id for the listbox user data since we don't have integer IDs - // Let's just put 0 for now since NGMP lobbies use string IDs (but the UI expects an Int). - // A cleaner solution later would be to map string IDs to integer handles. - GadgetListBoxSetItemData(win, reinterpret_cast(std::uintptr_t(1)), index); + GadgetListBoxSetItemData(win, reinterpret_cast(static_cast(i + 1)), index); AsciiString asciiMap(lobby.mapName.c_str()); UnicodeString uMap; diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 73106eec4ce..1a68dfe4edb 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -1113,12 +1113,12 @@ set(GAMEENGINE_SRC Source/GameNetwork/GUIUtil.cpp Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h Include/GameNetwork/GeneralsOnline/NGMP_Helpers.h - Include/GameNetwork/GeneralsOnline/NGMPChatSession.h + Include/GameNetwork/GeneralsOnline/NGMPWebSocket.h Include/GameNetwork/GeneralsOnline/ngmp_curl_utils.h Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp - Source/GameNetwork/GeneralsOnline/OnlineServices_Chat.cpp + Source/GameNetwork/GeneralsOnline/OnlineServices_WebSocket.cpp Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp # Source/GameNetwork/IPEnumeration.cpp # Source/GameNetwork/LANAPI.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPChatSession.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPChatSession.h deleted file mode 100644 index 22e3e2decd0..00000000000 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPChatSession.h +++ /dev/null @@ -1,49 +0,0 @@ -// GeneralsX @feature GeneralsOnline NGMP Chat WebSocket session header -// Manages a persistent WebSocket connection to the NGMP backend chat server. - -#ifndef NGMP_CHAT_SESSION_H -#define NGMP_CHAT_SESSION_H - -#include -#include -#include -#include -#include - -namespace NGMP { - -using ChatMessageCallback = std::function; - -class NGMPChatSession { -public: - NGMPChatSession() = default; - ~NGMPChatSession(); - - // Connect to the WebSocket endpoint (blocking until connected or failed) - bool connect(const std::string& wsUrl, const std::string& authToken); - - // Disconnect from the WebSocket and stop the receiver thread - void disconnect(); - - // Returns true if currently connected - bool isConnected() const { return m_running.load(); } - - // Send a chat message in the given room - bool sendMessage(const std::string& room, const std::string& message); - - // Set the callback invoked on the receiver thread when a message arrives - // NOTE: callback must post to the NGMP event queue, not touch UI directly - void setMessageCallback(ChatMessageCallback cb) { m_messageCallback = std::move(cb); } - -private: - void receiveLoop(); - - CURL* m_curl = nullptr; - std::thread m_recvThread; - std::atomic m_running = false; - ChatMessageCallback m_messageCallback; -}; - -} // namespace NGMP - -#endif // NGMP_CHAT_SESSION_H diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPWebSocket.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPWebSocket.h new file mode 100644 index 00000000000..9a1eaf38329 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPWebSocket.h @@ -0,0 +1,98 @@ +// GeneralsX @feature GeneralsOnline NGMP Chat WebSocket session header +// Manages a persistent WebSocket connection to the NGMP backend chat server. + +#ifndef NGMP_WEBSOCKET_H +#define NGMP_WEBSOCKET_H + +#include + +#pragma push_macro("min") +#pragma push_macro("max") +#undef min +#undef max + +#include +#include +#include +#include + +#pragma pop_macro("max") +#pragma pop_macro("min") + +namespace NGMP { + +enum class EWebSocketMessageID { + UNKNOWN = -1, + NETWORK_ROOM_CHAT_FROM_CLIENT = 1, + NETWORK_ROOM_CHAT_FROM_SERVER = 2, + NETWORK_ROOM_CHANGE_ROOM = 3, + NETWORK_ROOM_MEMBER_LIST_UPDATE = 4, + NETWORK_ROOM_MARK_READY = 5, + LOBBY_CURRENT_LOBBY_UPDATE = 6, + NETWORK_ROOM_LOBBY_LIST_UPDATE = 7, + ANTICHEAT_MESSAGE = 8, + PLAYER_NAME_CHANGE = 9, + LOBBY_ROOM_CHAT_FROM_CLIENT = 10, + LOBBY_CHAT_FROM_SERVER = 11, + NETWORK_SIGNAL = 12, + START_GAME = 13, + PING = 14, + PONG = 15, + PROBE = 16, + NETWORK_CONNECTION_START_SIGNALLING = 17, + NETWORK_CONNECTION_DISCONNECT_PLAYER = 18, + NETWORK_CONNECTION_CLIENT_REQUEST_SIGNALLING = 19, + MATCHMAKING_ACTION_JOIN_PREARRANGED_LOBBY = 20, + MATCHMAKING_ACTION_START_GAME = 21, + MATCHMAKING_MESSAGE = 22, + START_GAME_COUNTDOWN_STARTED = 23, + LOBBY_REMOVE_PASSWORD = 24, + LOBBY_CHANGE_PASSWORD = 25, + FULL_MESH_CONNECTIVITY_CHECK_HOST_REQUESTS_BEGIN = 26, + FULL_MESH_CONNECTIVITY_CHECK_RESPONSE = 27, + FULL_MESH_CONNECTIVITY_CHECK_RESPONSE_COMPLETE_TO_HOST = 28, + SOCIAL_NEW_FRIEND_REQUEST = 29, + SOCIAL_FRIEND_CHAT_MESSAGE_CLIENT_TO_SERVER = 30, + SOCIAL_FRIEND_CHAT_MESSAGE_SERVER_TO_CLIENT = 31, + SOCIAL_FRIEND_ONLINE_STATUS_CHANGED = 32, + SOCIAL_SUBSCRIBE_REALTIME_UPDATES = 33, + SOCIAL_UNSUBSCRIBE_REALTIME_UPDATES = 34, + SOCIAL_FRIENDS_OVERALL_STATUS_UPDATE = 35, + SOCIAL_FRIEND_FRIEND_REQUEST_ACCEPTED_BY_TARGET = 36, +}; + +using GenericMessageCallback = std::function; + +class NGMPWebSocket { +public: + NGMPWebSocket() = default; + ~NGMPWebSocket(); + + // Connect to the WebSocket endpoint (blocking until connected or failed) + bool connect(const std::string& wsUrl, const std::string& authToken); + + // Disconnect from the WebSocket and stop the receiver thread + void disconnect(); + + // Returns true if currently connected + bool isConnected() const { return m_running.load(); } + + // Send a generic string payload (like a serialized JSON) + bool sendPayload(const std::string& payload); + + // Set the generic callback invoked on the receiver thread when any message arrives + // The callback receives the raw JSON and must post to the NGMP event queue + void setMessageCallback(GenericMessageCallback cb) { m_messageCallback = std::move(cb); } + +private: + void receiveLoop(); + + CURL* m_curl = nullptr; + std::thread m_recvThread; + std::atomic m_running = false; + GenericMessageCallback m_messageCallback; +}; + +} // namespace NGMP + +#endif // NGMP_WEBSOCKET_H diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_json.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_json.h new file mode 100644 index 00000000000..1760102f18e --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_json.h @@ -0,0 +1,14 @@ +#ifndef NGMP_JSON_H +#define NGMP_JSON_H + +#pragma push_macro("min") +#pragma push_macro("max") +#undef min +#undef max + +#include + +#pragma pop_macro("max") +#pragma pop_macro("min") + +#endif // NGMP_JSON_H diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h index d4acee4f6a0..e456b46f344 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h @@ -4,9 +4,15 @@ #ifndef ONLINE_SERVICES_MANAGER_H #define ONLINE_SERVICES_MANAGER_H -#include "GameNetwork/GeneralsOnline/NGMPChatSession.h" +#include "GameNetwork/GeneralsOnline/NGMPWebSocket.h" #include "Common/GameDefines.h" #include "GameNetwork/GameSpy/PersistentStorageThread.h" + +#pragma push_macro("min") +#pragma push_macro("max") +#undef min +#undef max + #include #include #include @@ -17,6 +23,9 @@ #include #include +#pragma pop_macro("max") +#pragma pop_macro("min") + struct NGMPEvent { enum Type { EVENT_NONE, @@ -29,7 +38,12 @@ struct NGMPEvent { EVENT_CHAT_DISCONNECTED, EVENT_DISCONNECTED, EVENT_GLOBAL_STATS_RECEIVED, - EVENT_PLAYER_STATS_RECEIVED + EVENT_PLAYER_STATS_RECEIVED, + EVENT_WEBSOCKET_MESSAGE, + EVENT_PLAYLISTS_UPDATED, + EVENT_LOBBY_JOINED, + EVENT_LOBBY_CREATED, + EVENT_PLAYERS_UPDATED }; Type type = EVENT_NONE; @@ -37,11 +51,18 @@ struct NGMPEvent { }; struct NGMPLobby { - std::string id; + int64_t id; std::string name; std::string mapName; - int currentPlayers = 0; - int maxPlayers = 8; + int maxPlayers; + int currentPlayers; + bool hasPassword; +}; + +struct NGMPLobbyPlayer { + int64_t id; + std::string name; + bool isAdmin; }; struct GlobalStats { @@ -49,6 +70,25 @@ struct GlobalStats { std::vector matches; }; +struct PlaylistMapEntry { + std::string Name; + std::string Path; + bool Custom = false; +}; + +struct PlaylistEntry { + uint16_t PlaylistID = -1; + std::string Name; + int MinPlayers = -1; + int DesiredPlayers = -1; + int MinSelectedMaps = 0; + bool AllowTeams = false; + int TeamSize = -1; + bool AllowArmySelection = false; + uint16_t GracePeriodAtMinPlayersMSec = 0; + std::vector Maps; +}; + class NGMP_OnlineServicesManager { public: static NGMP_OnlineServicesManager& getInstance(); @@ -70,6 +110,14 @@ class NGMP_OnlineServicesManager { // Async lobby fetch — result delivered via EVENT_LOBBY_LIST_UPDATED void requestLobbyListAsync(); + void createLobbyAsync(const std::string& name, const std::string& mapName, const std::string& password, int maxPlayers); + void joinLobbyAsync(int64_t lobbyId, const std::string& password); + + // Async playlists fetch + void requestPlaylistsAsync(); + const std::vector& getPlaylists() const { return m_playlists; } + void startMatchmakingAsync(uint16_t playlistID, const std::vector& selectedMapIndexes); + void cancelMatchmakingAsync(); // Async stats fetch void requestGlobalStatsAsync(); @@ -80,11 +128,13 @@ class NGMP_OnlineServicesManager { bool getCachedPlayerStats(int64_t userID, PSPlayerStats& outStats) const; bool sendChatMessage(const std::string& room, const std::string& message); + void changeNetworkRoom(int16_t roomID); bool isLoggedIn() const { return m_isLoggedIn; } std::string getAuthToken() const { return m_authToken; } std::string getUsername() const { return m_username; } const std::vector& getLobbies() const { return m_lobbies; } + const std::vector& getLobbyPlayers() const { return m_lobbyPlayers; } // Internal thread-safe event poster (called from worker threads) void postEvent(const NGMPEvent& event); @@ -102,6 +152,7 @@ class NGMP_OnlineServicesManager { std::string m_authToken; std::string m_wsUri; std::vector m_lobbies; + std::vector m_lobbyPlayers; // Browser-based login state std::atomic m_waitingBrowserLogin = false; @@ -114,6 +165,11 @@ class NGMP_OnlineServicesManager { std::atomic m_lobbyRequestInFlight = false; std::thread m_lobbyThread; + // Matchmaking state + std::atomic m_playlistsRequestInFlight = false; + std::vector m_playlists; + std::thread m_playlistsThread; + // Async stats state std::atomic m_hasGlobalStats = false; std::atomic m_statsRequestInFlight = false; @@ -125,7 +181,7 @@ class NGMP_OnlineServicesManager { std::unordered_map m_cachedPlayerStats; // Chat WebSocket session - std::unique_ptr m_chatSession; + std::unique_ptr m_chatSession; mutable std::mutex m_eventMutex; std::queue m_eventQueue; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupHostGame.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupHostGame.cpp index f011b527b89..6a0df73b678 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupHostGame.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupHostGame.cpp @@ -71,6 +71,9 @@ #include "GameNetwork/GameSpy/LadderDefs.h" #include "Common/CustomMatchPreferences.h" #include "Common/LadderPreferences.h" +#if defined(SAGE_USE_NGMP) +#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" +#endif //----------------------------------------------------------------------------- // DEFINES //////////////////////////////////////////////////////////////////// @@ -614,5 +617,13 @@ void createGame() TheGameSpyGame->setLadderPort(req.stagingRoomCreation.ladPort); req.hostPingStr = TheGameSpyInfo->getPingString().str(); +#if defined(SAGE_USE_NGMP) + AsciiString aName; + aName.translate(gameName); + int maxPlayers = 8; + if (limitArmies) { maxPlayers = 4; } // Legacy fallback logic, not critical + NGMP_OnlineServicesManager::getInstance().createLobbyAsync(aName.str(), "Tournament Desert", passwd.str(), maxPlayers); +#else TheGameSpyPeerMessageQueue->addRequest(req); +#endif } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp index e6d7cc707e1..44cbe798ce6 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp @@ -669,6 +669,7 @@ void WOLLobbyMenuInit( WindowLayout *layout, void *userData ) // Show Menu layout->hide( FALSE ); +#ifndef SAGE_USE_NGMP // if we're not in a room, this will join the best available one if (!TheGameSpyInfo->getCurrentGroupRoom()) { @@ -688,14 +689,17 @@ void WOLLobbyMenuInit( WindowLayout *layout, void *userData ) { DEBUG_LOG(("WOLLobbyMenuInit() - not joining group room because we're already in one")); } +#endif GrabWindowInfo(); +#ifndef SAGE_USE_NGMP TheGameSpyInfo->clearStagingRoomList(); PeerRequest req; req.peerRequestType = PeerRequest::PEERREQUEST_STARTGAMELIST; req.gameList.restrictGameList = TheGameSpyConfig->restrictGamesToLobby(); TheGameSpyPeerMessageQueue->addRequest(req); +#endif // animate controls // TheShell->registerWithAnimateManager(parent, WIN_ANIMATION_SLIDE_TOP, TRUE); @@ -729,6 +733,7 @@ void WOLLobbyMenuInit( WindowLayout *layout, void *userData ) #if defined(SAGE_USE_NGMP) // GeneralsX @feature GeneralsOnline Kick off async lobby list refresh on menu init + NGMP_OnlineServicesManager::getInstance().changeNetworkRoom(0); NGMP_OnlineServicesManager::getInstance().requestLobbyListAsync(); #endif @@ -881,17 +886,16 @@ void refreshGameList( Bool forceRefresh ) if (forceRefresh || ((gameListRefreshTime == 0) || ((gameListRefreshTime + refreshInterval) <= timeGetTime()))) { +#if defined(SAGE_USE_NGMP) + NGMP_OnlineServicesManager::getInstance().requestLobbyListAsync(); + gameListRefreshTime = timeGetTime(); +#else if (TheGameSpyInfo->hasStagingRoomListChanged()) { - //DEBUG_LOG(("################### refreshing game list")); - //DEBUG_LOG(("gameRefreshTime=%d, refreshInterval=%d, now=%d", gameListRefreshTime, refreshInterval, timeGetTime())); RefreshGameListBoxes(); gameListRefreshTime = timeGetTime(); - } else { - //DEBUG_LOG(("-")); } - } else { - //DEBUG_LOG(("gameListRefreshTime: %d refreshInterval: %d", gameListRefreshTime, refreshInterval)); +#endif } } //------------------------------------------------------------------------------------------------- @@ -954,6 +958,23 @@ void WOLLobbyMenuUpdate( WindowLayout * layout, void *userData) uMsg.translate(msg); TheGameSpyInfo->addText(uMsg, GameSpyColor[GSCOLOR_DEFAULT], nullptr); } + else if (ev.type == NGMPEvent::EVENT_PLAYERS_UPDATED) { + TheGameSpyInfo->getPlayerInfoMap()->clear(); + for (const auto& player : NGMP_OnlineServicesManager::getInstance().getLobbyPlayers()) { + PlayerInfo info; + info.m_name = player.name.c_str(); + info.m_profileID = static_cast(player.id); + info.m_flags = player.isAdmin ? PEER_FLAG_OP : 0; + TheGameSpyInfo->getPlayerInfoMap()->insert(std::make_pair(info.m_name, info)); + } + refreshPlayerList(TRUE); + } + else if (ev.type == NGMPEvent::EVENT_LOBBY_JOINED || ev.type == NGMPEvent::EVENT_LOBBY_CREATED) { + SetLobbyAttemptHostJoin(FALSE); + buttonPushed = true; + nextScreen = "Menus/GameSpyGameOptionsMenu.wnd"; + TheShell->pop(); + } } #endif @@ -1584,10 +1605,25 @@ WindowMsgHandledType WOLLobbyMenuSystem( GameWindow *window, UnsignedInt msg, GadgetListBoxGetSelected(GetGameListBox(), &selected); if (selected >= 0) { - // GeneralsX @build BenderAI 12/02/2026 64-bit safe pointer cast Int selectedID = static_cast(reinterpret_cast(GadgetListBoxGetItemData(GetGameListBox(), selected))); if (selectedID > 0) { +#if defined(SAGE_USE_NGMP) + const auto& lobbies = NGMP_OnlineServicesManager::getInstance().getLobbies(); + size_t index = selectedID - 1; + if (index < lobbies.size()) + { + const auto& lobby = lobbies[index]; + UnicodeString uName; + AsciiString aName(lobby.name.c_str()); + uName.translate(aName); + TheGameSpyGame->setGameName(uName); + + // No password support for now in NGMP UI + NGMP_OnlineServicesManager::getInstance().joinLobbyAsync(lobby.id, ""); + SetLobbyAttemptHostJoin( TRUE ); + } +#else StagingRoomMap *srm = TheGameSpyInfo->getStagingRoomList(); StagingRoomMap::iterator srmIt = srm->find(selectedID); if (srmIt != srm->end()) @@ -1644,6 +1680,7 @@ WindowMsgHandledType WOLLobbyMenuSystem( GameWindow *window, UnsignedInt msg, TheGameSpyPeerMessageQueue->addRequest(req); } } +#endif } else { diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp index 3b1e67de325..8a267e6eff1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp @@ -39,6 +39,8 @@ #include "Common/PlayerTemplate.h" #include "GameClient/AnimateWindowManager.h" #include "GameClient/WindowLayout.h" +#include "GameClient/LanguageFilter.h" +#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" #include "GameClient/Gadget.h" #include "GameClient/GameText.h" #include "GameClient/InGameUI.h" @@ -1026,6 +1028,31 @@ void WOLQuickMatchMenuUpdate( WindowLayout * layout, void *userData) raiseMessageBoxes = false; } +#if defined(SAGE_USE_NGMP) + // Process NGMP Events + auto events = NGMP_OnlineServicesManager::getInstance().pollEvents(); + for (const auto& ev : events) { + if (ev.type == NGMPEvent::EVENT_PLAYLISTS_UPDATED) { + const auto& playlists = NGMP_OnlineServicesManager::getInstance().getPlaylists(); + GadgetComboBoxReset(comboBoxNumPlayers); + for (const auto& pl : playlists) { + UnicodeString s; + s.format(L"%hs", pl.Name.c_str()); + GadgetComboBoxAddEntry(comboBoxNumPlayers, s, GameSpyColor[GSCOLOR_DEFAULT]); + } + if (!playlists.empty()) { + GadgetComboBoxSetSelectedPos(comboBoxNumPlayers, 0); + QuickMatchPreferences pref; + populateQuickMatchMapSelectListbox(pref); + UpdateStartButton(); + } + } else if (ev.type == NGMPEvent::EVENT_WEBSOCKET_MESSAGE) { + // For match found, start game, matchmaking error, etc. + // TODO: Implement parsing for these events in Phase 3. + } + } +#endif + /// @todo: MDC handle disconnects in-game the same way as Custom Match! if (TheShell->isAnimFinished() && !buttonPushed && TheGameSpyPeerMessageQueue) @@ -1610,6 +1637,14 @@ WindowMsgHandledType WOLQuickMatchMenuSystem( GameWindow *window, UnsignedInt ms if ( controlID == buttonStopID ) { +#if defined(SAGE_USE_NGMP) + NGMP_OnlineServicesManager::getInstance().cancelMatchmakingAsync(); + buttonWiden->winEnable(FALSE); + buttonStart->winHide(FALSE); + buttonStop->winHide(TRUE); + enableOptionsGadgets(TRUE); + buttonBack->winEnable(TRUE); +#else PeerRequest req; req.peerRequestType = PeerRequest::PEERREQUEST_STOPQUICKMATCH; TheGameSpyPeerMessageQueue->addRequest(req); @@ -1618,6 +1653,7 @@ WindowMsgHandledType WOLQuickMatchMenuSystem( GameWindow *window, UnsignedInt ms buttonStop->winHide( TRUE ); enableOptionsGadgets(TRUE); TheGameSpyInfo->addText(TheGameText->fetch("GUI:QMAborted"), GameSpyColor[GSCOLOR_DEFAULT], quickmatchTextWindow); +#endif } else if ( controlID == buttonOptionsID ) { @@ -1644,6 +1680,24 @@ WindowMsgHandledType WOLQuickMatchMenuSystem( GameWindow *window, UnsignedInt ms } else if ( controlID == buttonStartID ) { +#if defined(SAGE_USE_NGMP) + std::vector selectedMaps; + Int selected = -1; + GadgetComboBoxGetSelectedPos(comboBoxNumPlayers, &selected); + if (selected >= 0) { + const auto& playlists = NGMP_OnlineServicesManager::getInstance().getPlaylists(); + if (selected < (Int)playlists.size()) { + NGMP_OnlineServicesManager::getInstance().startMatchmakingAsync(playlists[selected].PlaylistID, selectedMaps); + + buttonWiden->winEnable(TRUE); + buttonStart->winHide(TRUE); + buttonStop->winHide(FALSE); + buttonStop->winEnable(TRUE); + enableOptionsGadgets(FALSE); + buttonBack->winEnable(FALSE); + } + } +#else PeerRequest req; req.peerRequestType = PeerRequest::PEERREQUEST_STARTQUICKMATCH; req.qmMaps.clear(); @@ -1819,6 +1873,7 @@ WindowMsgHandledType WOLQuickMatchMenuSystem( GameWindow *window, UnsignedInt ms ladPref.addRecentLadder( p ); ladPref.write(); } +#endif } else if ( controlID == buttonBuddiesID ) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp index 10ff457490a..402626e1d95 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp @@ -623,6 +623,9 @@ void WOLWelcomeMenuInit( WindowLayout *layout, void *userData ) fprintf(stderr, "[WOLWelcomeMenuInit] enableControls()...\n"); fflush(stderr); +#if defined(SAGE_USE_NGMP) + enableControls( NGMP_OnlineServicesManager::getInstance().isLoggedIn() ); +#else if (TheGameSpyInfo) { enableControls( TheGameSpyInfo->gotGroupRoomList() ); } else { @@ -630,6 +633,7 @@ void WOLWelcomeMenuInit( WindowLayout *layout, void *userData ) fflush(stderr); enableControls( false ); } +#endif fprintf(stderr, "[WOLWelcomeMenuInit] showShellMap()...\n"); fflush(stderr); @@ -975,8 +979,14 @@ WindowMsgHandledType WOLWelcomeMenuSystem( GameWindow *window, UnsignedInt msg, { //TheGameSpyChat->clearGroupRoomList(); //peerListGroupRooms(TheGameSpyChat->getPeer(), ListGroupRoomsCallback, nullptr, PEERTrue); +#if defined(SAGE_USE_NGMP) + buttonPushed = TRUE; + nextScreen = "Menus/WOLCustomLobby.wnd"; + TheShell->pop(); +#else TheGameSpyInfo->joinBestGroupRoom(); enableControls( FALSE ); +#endif /* diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp index f589e373214..c1a03253807 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp @@ -9,7 +9,7 @@ #include #include #include -#include +#include "GameNetwork/GeneralsOnline/NGMP_json.h" using json = nlohmann::json; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp index 5a7c9124a7c..cec8cf4fc06 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp @@ -4,11 +4,11 @@ #include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" #include "GameNetwork/GeneralsOnline/NGMP_Helpers.h" #include "GameNetwork/GeneralsOnline/ngmp_curl_utils.h" -#include "GameNetwork/GeneralsOnline/NGMPChatSession.h" +#include "GameNetwork/GeneralsOnline/NGMPWebSocket.h" #include #include #include -#include +#include "GameNetwork/GeneralsOnline/NGMP_json.h" using json = nlohmann::json; @@ -44,7 +44,13 @@ std::vector NGMP_OnlineServicesManager::pollEvents() { fprintf(stderr, "[NGMP-MainThread] Event: Auth Success\n"); m_isLoggedIn = true; if (!m_chatSession) { - m_chatSession.reset(new NGMP::NGMPChatSession()); + m_chatSession.reset(new NGMP::NGMPWebSocket()); + m_chatSession->setMessageCallback([this](const std::string& rawJson) { + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_WEBSOCKET_MESSAGE; + ev.payload = rawJson; + postEvent(ev); + }); } m_chatSession->connect(m_wsUri, m_authToken); break; @@ -63,10 +69,57 @@ std::vector NGMP_OnlineServicesManager::pollEvents() { case NGMPEvent::EVENT_CHAT_DISCONNECTED: fprintf(stderr, "[NGMP-MainThread] Event: Chat disconnected\n"); break; + case NGMPEvent::EVENT_WEBSOCKET_MESSAGE: + { + try { + auto jsonMsg = nlohmann::json::parse(ev.payload); + if (jsonMsg.contains("msg_id") && jsonMsg["msg_id"].is_number_integer()) { + int msgId = jsonMsg["msg_id"].get(); + + if (msgId == 2) { // NETWORK_ROOM_CHAT_FROM_SERVER + std::string msgText = ""; + if (jsonMsg.contains("message") && jsonMsg["message"].is_string()) { + msgText = jsonMsg["message"].get(); + } + NGMPEvent chatEv; + chatEv.type = NGMPEvent::EVENT_CHAT_MESSAGE_RECEIVED; + chatEv.payload = msgText; + events.push_back(chatEv); + } + else if (msgId == 4) { // NETWORK_ROOM_MEMBER_LIST_UPDATE + if (jsonMsg.contains("members") && jsonMsg["members"].is_array()) { + std::vector updatedPlayers; + for (const auto& member : jsonMsg["members"]) { + NGMPLobbyPlayer player; + player.id = member.value("UserID", 0LL); + player.name = member.value("Name", ""); + player.isAdmin = member.value("IsAdmin", false); + updatedPlayers.push_back(player); + } + { + std::lock_guard lock(m_eventMutex); + m_lobbyPlayers = std::move(updatedPlayers); + } + NGMPEvent playersEv; + playersEv.type = NGMPEvent::EVENT_PLAYERS_UPDATED; + events.push_back(playersEv); + } + } + else if (msgId == 7) { // NETWORK_ROOM_LOBBY_LIST_UPDATE + requestLobbyListAsync(); + } + } + } catch (...) { + fprintf(stderr, "[NGMP] Failed to parse WS message: %s\n", ev.payload.c_str()); + } + } + break; default: break; } - events.push_back(ev); + if (ev.type != NGMPEvent::EVENT_WEBSOCKET_MESSAGE) { + events.push_back(ev); + } } return events; } @@ -89,31 +142,43 @@ void NGMP_OnlineServicesManager::requestLobbyListAsync() { return; } - std::string url = NGMP::GetServerRESTEndpoint() + "/lobbies"; + std::string url = NGMP::GetAPIEndpoint("Lobbies"); NGMP::Internal::CurlResponse response; + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + std::string authHeader = "Authorization: Bearer " + m_authToken; + headers = curl_slist_append(headers, authHeader.c_str()); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NGMP::Internal::WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + + fprintf(stderr, "[NGMP-DEBUG] requestLobbyListAsync sending Token: %s\n", m_authToken.c_str()); + fflush(stderr); CURLcode res = curl_easy_perform(curl); long httpCode = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + curl_slist_free_all(headers); curl_easy_cleanup(curl); if (res == CURLE_OK && httpCode == 200) { try { auto jsonList = json::parse(response.text); std::vector lobbies; - if (jsonList.is_array()) { - for (const auto& item : jsonList) { + if (jsonList.contains("lobbies") && jsonList["lobbies"].is_array()) { + for (const auto& item : jsonList["lobbies"]) { NGMPLobby lobby; - lobby.id = item.value("id", ""); + lobby.id = item.value("lobbyID", 0LL); lobby.name = item.value("name", "Custom Lobby"); - lobby.mapName = item.value("mapName", "Tournament Desert"); - lobby.currentPlayers = item.value("currentPlayers", 1); - lobby.maxPlayers = item.value("maxPlayers", 8); + lobby.mapName = item.value("map_name", "Tournament Desert"); + lobby.currentPlayers = item.value("current_players", 1); + lobby.maxPlayers = item.value("max_players", 8); + lobby.hasPassword = item.value("has_password", false); lobbies.push_back(lobby); } } @@ -136,10 +201,286 @@ void NGMP_OnlineServicesManager::requestLobbyListAsync() { fflush(stderr); } - m_lobbyRequestInFlight = false; + m_lobbyRequestInFlight = false; }); } +void NGMP_OnlineServicesManager::createLobbyAsync(const std::string& name, const std::string& mapName, const std::string& password, int maxPlayers) { + std::thread([this, name, mapName, password, maxPlayers]() { + CURL* curl = curl_easy_init(); + if (!curl) return; + + std::string url = NGMP::GetAPIEndpoint("Lobbies"); + NGMP::Internal::CurlResponse response; + + json payload = { + {"name", name}, + {"map_name", mapName}, + {"map_path", mapName}, // Fallback for map path + {"map_official", true}, + {"max_players", maxPlayers}, + {"preferred_port", 0}, + {"vanilla_teams", false}, + {"track_stats", false}, + {"starting_cash", 10000}, + {"passworded", !password.empty()}, + {"allow_observers", true}, + {"max_cam_height", 300}, + {"exe_crc", 0}, + {"ini_crc", 0}, + {"anticheat_id", 0} + }; + if (!password.empty()) { + payload["password"] = password; + } + std::string payloadStr = payload.dump(); + + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + std::string authHeader = "Authorization: Bearer " + m_authToken; + headers = curl_slist_append(headers, authHeader.c_str()); + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payloadStr.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NGMP::Internal::WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + + CURLcode res = curl_easy_perform(curl); + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (res == CURLE_OK && httpCode == 200) { + requestLobbyListAsync(); + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_LOBBY_CREATED; + postEvent(ev); + } else { + fprintf(stderr, "[NGMP] Create lobby failed (curl=%d, http=%ld)\n", res, httpCode); + fflush(stderr); + } + }).detach(); +} + +void NGMP_OnlineServicesManager::joinLobbyAsync(int64_t lobbyId, const std::string& password) { + std::thread([this, lobbyId, password]() { + CURL* curl = curl_easy_init(); + if (!curl) return; + + std::string url = NGMP::GetAPIEndpoint(("Lobby/" + std::to_string(lobbyId)).c_str()); + NGMP::Internal::CurlResponse response; + + json payload = { + {"preferred_port", 0}, + {"anticheat_id", 0}, + {"has_map", true} + }; + if (!password.empty()) { + payload["password"] = password; + } + std::string payloadStr = payload.dump(); + + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + std::string authHeader = "Authorization: Bearer " + m_authToken; + headers = curl_slist_append(headers, authHeader.c_str()); + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payloadStr.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NGMP::Internal::WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + + CURLcode res = curl_easy_perform(curl); + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (res == CURLE_OK && httpCode == 200) { + requestLobbyListAsync(); + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_LOBBY_JOINED; + postEvent(ev); + } else { + fprintf(stderr, "[NGMP] Join lobby failed (curl=%d, http=%ld)\n", res, httpCode); + fflush(stderr); + } + }).detach(); +} + +void NGMP_OnlineServicesManager::requestPlaylistsAsync() { + if (m_playlistsRequestInFlight.exchange(true)) { + fprintf(stderr, "[NGMP] Playlists request already in flight, ignoring duplicate\n"); + fflush(stderr); + return; + } + + if (m_playlistsThread.joinable()) { + m_playlistsThread.join(); + } + + m_playlistsThread = std::thread([this]() { + CURL* curl = curl_easy_init(); + if (!curl) { + m_playlistsRequestInFlight = false; + return; + } + + std::string url = NGMP::GetAPIEndpoint("matchmaking/playlists"); + NGMP::Internal::CurlResponse response; + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NGMP::Internal::WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); + + CURLcode res = curl_easy_perform(curl); + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + curl_easy_cleanup(curl); + + if (res == CURLE_OK && httpCode == 200) { + try { + auto jsonList = json::parse(response.text); + std::vector playlists; + if (jsonList.is_array()) { + for (const auto& item : jsonList) { + PlaylistEntry entry; + entry.PlaylistID = item.value("playlistID", -1); + entry.Name = item.value("name", "Unknown Playlist"); + entry.MinPlayers = item.value("minPlayers", 2); + entry.DesiredPlayers = item.value("desiredPlayers", 2); + entry.MinSelectedMaps = item.value("minSelectedMaps", 1); + entry.AllowTeams = item.value("allowTeams", false); + entry.TeamSize = item.value("teamSize", -1); + entry.AllowArmySelection = item.value("allowArmySelection", true); + entry.GracePeriodAtMinPlayersMSec = item.value("gracePeriodAtMinPlayersMSec", 0); + + auto mapsArr = item.value("maps", json::array()); + for (const auto& mapItem : mapsArr) { + PlaylistMapEntry mapEntry; + mapEntry.Name = mapItem.value("name", ""); + mapEntry.Path = mapItem.value("path", ""); + mapEntry.Custom = mapItem.value("custom", false); + entry.Maps.push_back(mapEntry); + } + playlists.push_back(entry); + } + } + + // Swap into member under the event mutex for safe handoff + { + std::lock_guard lock(m_eventMutex); + m_playlists = std::move(playlists); + } + + NGMPEvent ev; + ev.type = NGMPEvent::EVENT_PLAYLISTS_UPDATED; + postEvent(ev); + } catch (const std::exception& e) { + fprintf(stderr, "[NGMP] Playlists JSON parse exception: %s\n", e.what()); + fflush(stderr); + } + } else { + fprintf(stderr, "[NGMP] Playlists request failed (curl=%d, http=%ld)\n", res, httpCode); + fflush(stderr); + } + + m_playlistsRequestInFlight = false; + }); +} + +void NGMP_OnlineServicesManager::startMatchmakingAsync(uint16_t playlistID, const std::vector& selectedMapIndexes) { + std::thread([this, playlistID, selectedMapIndexes]() { + CURL* curl = curl_easy_init(); + if (!curl) return; + + json payload; + payload["playlist"] = playlistID; + payload["maps"] = selectedMapIndexes; + // payload["exe_crc"] = TheGlobalData->m_exeCRC; + // payload["ini_crc"] = TheGlobalData->m_iniCRC; + // payload["anticheat_id"] = ""; + + std::string payloadStr = payload.dump(); + std::string url = NGMP::GetAPIEndpoint("matchmaking"); + NGMP::Internal::CurlResponse response; + + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + std::string authHeader = "Authorization: Bearer " + m_authToken; + headers = curl_slist_append(headers, authHeader.c_str()); + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payloadStr.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NGMP::Internal::WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); + + CURLcode res = curl_easy_perform(curl); + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (res == CURLE_OK && httpCode == 201) { + fprintf(stderr, "[NGMP] Matchmaking started successfully\n"); + fflush(stderr); + } else { + fprintf(stderr, "[NGMP] Failed to start matchmaking (curl=%d, http=%ld)\n", res, httpCode); + fflush(stderr); + } + }).detach(); +} + +void NGMP_OnlineServicesManager::cancelMatchmakingAsync() { + std::thread([this]() { + CURL* curl = curl_easy_init(); + if (!curl) return; + + std::string url = NGMP::GetAPIEndpoint("matchmaking"); + NGMP::Internal::CurlResponse response; + + struct curl_slist* headers = nullptr; + std::string authHeader = "Authorization: Bearer " + m_authToken; + headers = curl_slist_append(headers, authHeader.c_str()); + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NGMP::Internal::WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); + + CURLcode res = curl_easy_perform(curl); + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (res == CURLE_OK && httpCode == 200) { + fprintf(stderr, "[NGMP] Matchmaking cancelled successfully\n"); + fflush(stderr); + } else { + fprintf(stderr, "[NGMP] Failed to cancel matchmaking (curl=%d, http=%ld)\n", res, httpCode); + fflush(stderr); + } + }).detach(); +} + bool NGMP_OnlineServicesManager::hasGlobalStats() const { return m_hasGlobalStats; } @@ -154,9 +495,28 @@ bool NGMP_OnlineServicesManager::sendChatMessage(const std::string& room, const return false; } if (m_chatSession) { - return m_chatSession->sendMessage(room, message); + // Build the chat message payload + nlohmann::json payload = { + {"msg_id", 1}, + {"action", "chat"}, + {"message", message} + }; + return m_chatSession->sendPayload(payload.dump()); } fprintf(stderr, "[NGMP] sendChatMessage called but no active chat session\n"); fflush(stderr); return false; } + +void NGMP_OnlineServicesManager::changeNetworkRoom(int16_t roomID) { + if (!m_isLoggedIn) { + return; + } + if (m_chatSession) { + nlohmann::json payload = { + {"msg_id", 3}, // NETWORK_ROOM_CHANGE_ROOM + {"room", roomID} + }; + m_chatSession->sendPayload(payload.dump()); + } +} diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Chat.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_WebSocket.cpp similarity index 66% rename from GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Chat.cpp rename to GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_WebSocket.cpp index fe5156d3c3f..cd11907ed04 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Chat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_WebSocket.cpp @@ -1,20 +1,20 @@ // GeneralsX @feature GeneralsOnline NGMP Chat WebSocket implementation // Persistent WS connection using libcurl WebSocket (>= 7.86.0) for bidirectional chat. -#include "GameNetwork/GeneralsOnline/NGMPChatSession.h" +#include "GameNetwork/GeneralsOnline/NGMPWebSocket.h" #include #include -#include +#include "GameNetwork/GeneralsOnline/NGMP_json.h" using json = nlohmann::json; namespace NGMP { -NGMPChatSession::~NGMPChatSession() { +NGMPWebSocket::~NGMPWebSocket() { disconnect(); } -bool NGMPChatSession::connect(const std::string& wsUrl, const std::string& authToken) { +bool NGMPWebSocket::connect(const std::string& wsUrl, const std::string& authToken) { if (m_running.load()) { return true; // Already connected } @@ -58,11 +58,11 @@ bool NGMPChatSession::connect(const std::string& wsUrl, const std::string& authT fflush(stderr); m_running = true; - m_recvThread = std::thread(&NGMPChatSession::receiveLoop, this); + m_recvThread = std::thread(&NGMPWebSocket::receiveLoop, this); return true; } -void NGMPChatSession::disconnect() { +void NGMPWebSocket::disconnect() { m_running = false; if (m_recvThread.joinable()) { m_recvThread.join(); @@ -75,33 +75,36 @@ void NGMPChatSession::disconnect() { fflush(stderr); } -bool NGMPChatSession::sendMessage(const std::string& room, const std::string& message) { +bool NGMPWebSocket::sendPayload(const std::string& payload) { if (!m_running.load() || !m_curl) { return false; } - json payload = { - {"type", "chat"}, - {"room", room}, - {"message", message} - }; - std::string frame = payload.dump(); - size_t sent = 0; - CURLcode res = curl_ws_send(m_curl, frame.c_str(), frame.size(), &sent, 0, CURLWS_TEXT); + CURLcode res = curl_ws_send(m_curl, payload.c_str(), payload.size(), &sent, 0, CURLWS_TEXT); if (res != CURLE_OK) { - fprintf(stderr, "[NGMP-Chat] Failed to send WS message: %s\n", curl_easy_strerror(res)); + fprintf(stderr, "[NGMP-WebSocket] Failed to send WS payload: %s\n", curl_easy_strerror(res)); fflush(stderr); return false; } return true; } -void NGMPChatSession::receiveLoop() { +void NGMPWebSocket::receiveLoop() { char buffer[4096]; const struct curl_ws_frame* meta = nullptr; + auto lastPingTime = std::chrono::steady_clock::now(); + while (m_running.load()) { + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - lastPingTime).count() >= 10) { + lastPingTime = now; + std::string pingPayload = "{\"msg_id\":8}"; + size_t sent = 0; + curl_ws_send(m_curl, pingPayload.c_str(), pingPayload.size(), &sent, 0, CURLWS_TEXT); + } + size_t received = 0; CURLcode res = curl_ws_recv(m_curl, buffer, sizeof(buffer) - 1, &received, &meta); @@ -120,18 +123,8 @@ void NGMPChatSession::receiveLoop() { if (received > 0) { buffer[received] = '\0'; - try { - auto msg = json::parse(buffer); - std::string type = msg.value("type", ""); - if (type == "chat" && m_messageCallback) { - std::string room = msg.value("room", ""); - std::string sender = msg.value("sender", ""); - std::string content = msg.value("message", ""); - m_messageCallback(room, sender, content); - } - } catch (const std::exception& e) { - fprintf(stderr, "[NGMP-Chat] JSON parse error in WS frame: %s\n", e.what()); - fflush(stderr); + if (m_messageCallback) { + m_messageCallback(std::string(buffer)); } } } From 034023d440a0bd6639bd5eb8d9537c213a2c22a3 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 12 Aug 2026 23:06:18 -0300 Subject: [PATCH 19/51] fix(ngmp): prevent SIGSEGV on 'Create Game' via staging room init ordering WOLLobbyMenuUpdate was calling TheShell->pop() before calling markAsStagingRoomHost() in the EVENT_LOBBY_CREATED handler. Because buttonPushed=true at that point, Shell::pop() triggers WOLLobbyMenuShutdown with popImmediate=TRUE, which synchronously executes shutdownComplete -> TheShell->push -> WOLGameSetupMenuInit. Inside WOLGameSetupMenuInit, getCurrentStagingRoom() returned nullptr (m_isHosting was still FALSE), causing a SIGSEGV on game->getSlot(0). Fix: call markAsStagingRoomHost()/markAsStagingRoomJoiner() BEFORE TheShell->pop() so the staging room state is initialized before any synchronous init chain can fire. Also add a defensive null-check in WOLGameSetupMenuInit for getCurrentStagingRoom() that pops back to the lobby if nil, preventing any future crash from an unexpected state loss. Add diagnostic stderr logs in PopupHostGame and WOLLobbyMenu to trace createLobbyAsync invocation and staging room transitions. --- .../GUI/GUICallbacks/Menus/PopupHostGame.cpp | 3 ++ .../GUICallbacks/Menus/WOLGameSetupMenu.cpp | 10 +++++ .../GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp | 40 +++++++++++++++++-- docs/WORKLOG/2026-08-DIARY.md | 11 +++++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupHostGame.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupHostGame.cpp index 6a0df73b678..8e5fd52e3db 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupHostGame.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/PopupHostGame.cpp @@ -622,6 +622,9 @@ void createGame() aName.translate(gameName); int maxPlayers = 8; if (limitArmies) { maxPlayers = 4; } // Legacy fallback logic, not critical + fprintf(stderr, "[NGMP] PopupHostGame: invoking createLobbyAsync name='%s' maxPlayers=%d\n", + aName.str(), maxPlayers); + fflush(stderr); NGMP_OnlineServicesManager::getInstance().createLobbyAsync(aName.str(), "Tournament Desert", passwd.str(), maxPlayers); #else TheGameSpyPeerMessageQueue->addRequest(req); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp index 062524442c8..e22e6d08af0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -1366,6 +1366,16 @@ void WOLGameSetupMenuInit( WindowLayout *layout, void *userData ) //The dialog needs to react differently depending on whether it's the host or not. TheMapCache->updateCache(); GameSpyStagingRoom *game = TheGameSpyInfo->getCurrentStagingRoom(); + // GeneralsX @bugfix fbraz3 12/08/2026 Defensive null-check: getCurrentStagingRoom() returns nullptr + // if m_isHosting and m_joinedStagingRoom are both false (state was never set or was reset). + // This can happen if markAsStagingRoomHost/Joiner wasn't called before the shell transition. + if (!game) { + fprintf(stderr, "[NGMP] WOLGameSetupMenuInit: getCurrentStagingRoom() returned nullptr! " + "m_isHosting/m_joinedStagingRoom not set. Popping back to lobby.\n"); + fflush(stderr); + TheShell->popImmediate(); + return; + } GameSpyGameSlot *hostSlot = game->getGameSpySlot(0); hostSlot->setAccept(); if (TheGameSpyInfo->amIHost()) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp index 44cbe798ce6..8bb8a3967e3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp @@ -94,6 +94,9 @@ static time_t gameListRefreshTime = 0; static const time_t gameListRefreshInterval = 10000; static time_t playerListRefreshTime = 0; static const time_t playerListRefreshInterval = 5000; +#if defined(SAGE_USE_NGMP) +static Bool ngmpRoomConfirmed = FALSE; // Blocks lobby refresh until server confirms room 0 (msg_id:4) +#endif void setUnignoreText( WindowLayout *layout, AsciiString nick, GPProfile id); static void doSliderTrack(GameWindow *control, Int val); @@ -699,6 +702,9 @@ void WOLLobbyMenuInit( WindowLayout *layout, void *userData ) req.peerRequestType = PeerRequest::PEERREQUEST_STARTGAMELIST; req.gameList.restrictGameList = TheGameSpyConfig->restrictGamesToLobby(); TheGameSpyPeerMessageQueue->addRequest(req); +#else + // In NGMP, we always join room 0 for the main custom lobby + NGMP_OnlineServicesManager::getInstance().changeNetworkRoom(0); #endif // animate controls @@ -732,9 +738,9 @@ void WOLLobbyMenuInit( WindowLayout *layout, void *userData ) DontShowMainMenu = TRUE; #if defined(SAGE_USE_NGMP) - // GeneralsX @feature GeneralsOnline Kick off async lobby list refresh on menu init + // GeneralsX @feature GeneralsOnline Enter global lobby room 0; block lobby refresh until server confirms room change + ngmpRoomConfirmed = FALSE; NGMP_OnlineServicesManager::getInstance().changeNetworkRoom(0); - NGMP_OnlineServicesManager::getInstance().requestLobbyListAsync(); #endif } @@ -887,13 +893,18 @@ void refreshGameList( Bool forceRefresh ) if (forceRefresh || ((gameListRefreshTime == 0) || ((gameListRefreshTime + refreshInterval) <= timeGetTime()))) { #if defined(SAGE_USE_NGMP) - NGMP_OnlineServicesManager::getInstance().requestLobbyListAsync(); - gameListRefreshTime = timeGetTime(); + // GeneralsX @bugfix GeneralsOnline Do not request lobby list until the server has confirmed we are in room 0 + if (ngmpRoomConfirmed || forceRefresh) + { + NGMP_OnlineServicesManager::getInstance().requestLobbyListAsync(); + gameListRefreshTime = timeGetTime(); + } #else if (TheGameSpyInfo->hasStagingRoomListChanged()) { RefreshGameListBoxes(); gameListRefreshTime = timeGetTime(); + } else { } #endif } @@ -968,11 +979,32 @@ void WOLLobbyMenuUpdate( WindowLayout * layout, void *userData) TheGameSpyInfo->getPlayerInfoMap()->insert(std::make_pair(info.m_name, info)); } refreshPlayerList(TRUE); + // Server confirmed room change: unlock lobby refresh and fetch immediately + if (!ngmpRoomConfirmed) { + ngmpRoomConfirmed = TRUE; + NGMP_OnlineServicesManager::getInstance().requestLobbyListAsync(); + } } else if (ev.type == NGMPEvent::EVENT_LOBBY_JOINED || ev.type == NGMPEvent::EVENT_LOBBY_CREATED) { SetLobbyAttemptHostJoin(FALSE); buttonPushed = true; nextScreen = "Menus/GameSpyGameOptionsMenu.wnd"; + // GeneralsX @bugfix fbraz3 12/08/2026 Initialize staging room state BEFORE TheShell->pop(). + // pop() may trigger WOLLobbyMenuShutdown with popImmediate=TRUE (because buttonPushed=true), + // which synchronously calls shutdownComplete -> TheShell->push -> WOLGameSetupMenuInit. + // If markAsStagingRoomHost/Joiner were called AFTER pop(), getCurrentStagingRoom() would + // return nullptr in WOLGameSetupMenuInit, causing a SIGSEGV at game->getSlot(0). + if (ev.type == NGMPEvent::EVENT_LOBBY_CREATED) { + fprintf(stderr, "[NGMP] EVENT_LOBBY_CREATED: marking as staging room host before shell pop\n"); + fflush(stderr); + TheGameSpyInfo->markAsStagingRoomHost(); + TheGameSpyInfo->setGameOptions(); + } else { + fprintf(stderr, "[NGMP] EVENT_LOBBY_JOINED: marking as staging room joiner before shell pop\n"); + fflush(stderr); + // Initialize the joined staging room so getCurrentStagingRoom() doesn't return nullptr + TheGameSpyInfo->markAsStagingRoomJoiner(0); + } TheShell->pop(); } } diff --git a/docs/WORKLOG/2026-08-DIARY.md b/docs/WORKLOG/2026-08-DIARY.md index 5a4b03caf39..d4986eec5f2 100644 --- a/docs/WORKLOG/2026-08-DIARY.md +++ b/docs/WORKLOG/2026-08-DIARY.md @@ -39,3 +39,14 @@ - Fixed the Communicator (buddy overlay) closing automatically by modifying `WOLBuddyOverlay.cpp` and `GameSpyOverlay.cpp` to use `NGMP_OnlineServicesManager::getInstance().isLoggedIn()` instead of legacy GameSpy checks. - Set up `GameSpyPlayerInfoOverlayUpdate` and `PopupPlayerInfo.cpp` to natively wait for stats using a boolean flag `g_waitingForPlayerStats`, updating the UI re-actively once `EVENT_PLAYER_STATS_RECEIVED` is fired. - Adapted `PSPlayerStats` deserialization from reference implementation to match GeneralsX layout (removed `elo_rating` and `elo_num_matches`). + +## 2026-08-12 + +### NGMP: Segfault fix when clicking "Create Game" (WOLGameSetupMenuInit) + +**Root cause**: `WOLLobbyMenuUpdate` was calling `TheShell->pop()` *before* `TheGameSpyInfo->markAsStagingRoomHost()` when handling `EVENT_LOBBY_CREATED`. Because `buttonPushed=true` was set on the same line, `Shell::pop()` triggers `WOLLobbyMenuShutdown` with `popImmediate=TRUE`, which synchronously calls `shutdownComplete` → `TheShell->push("GameSpyGameOptionsMenu")` → `WOLGameSetupMenuInit`. At that point, `getCurrentStagingRoom()` returned `nullptr` (since `m_isHosting=FALSE`), causing a SIGSEGV on `game->getSlot(0)`. + +**Fix**: +- `WOLLobbyMenu.cpp`: Moved `markAsStagingRoomHost()` / `markAsStagingRoomJoiner()` to execute **before** `TheShell->pop()` in the `EVENT_LOBBY_CREATED` and `EVENT_LOBBY_JOINED` handler. +- `WOLGameSetupMenu.cpp`: Added defensive null-check for `getCurrentStagingRoom()` return value to prevent future crashes if state is somehow lost; logs to stderr and pops back to lobby. +- `PopupHostGame.cpp`: Added diagnostic `fprintf(stderr, ...)` log before `createLobbyAsync` call to trace invocation. From c48fb4c7940050c2b11226e49f25826f7bb8143f Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 12 Aug 2026 23:20:25 -0300 Subject: [PATCH 20/51] fix(ngmp): separate UI events from GameEngine internal tick update GameEngine.cpp was calling pollEvents() every frame, which consumed and discarded all pending UI events (like EVENT_LOBBY_CREATED) if they arrived between menu updates. This caused a race condition where the 'Create Game' flow would successfully create a lobby on the server, but the UI menu would never transition to the staging room setup screen. Fix: Split pollEvents() into update() and pollEvents(). - update(): Processes internal logic (WebSocket messages) and moves UI events to a new m_uiEventQueue. - pollEvents(): Now exclusively polls m_uiEventQueue for the UI menus. - GameEngine::update() now correctly calls NGMP_OnlineServicesManager::update() instead of pollEvents(). Also added fallback for PascalCase vs camelCase in lobby parsing (Name/name) and added a diagnostic log to capture the Lobbies API JSON response. --- .../GeneralsOnline/OnlineServices_Manager.h | 3 + .../GameEngine/Source/Common/GameEngine.cpp | 2 +- .../GeneralsOnline/OnlineServices_Manager.cpp | 61 +++++++++++++------ 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h index e456b46f344..af4e4c2feae 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h @@ -95,6 +95,8 @@ class NGMP_OnlineServicesManager { bool init(); std::vector pollEvents(); // Main thread UI tick dispatch + void update(); // Main thread internal state tick + void shutdown(); // Browser-based gamecode login flow (macOS/Linux: uses SDL_OpenURL) @@ -185,6 +187,7 @@ class NGMP_OnlineServicesManager { mutable std::mutex m_eventMutex; std::queue m_eventQueue; + std::queue m_uiEventQueue; }; #endif // ONLINE_SERVICES_MANAGER_H diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index 12d4f2cd2f9..2277bd21dc3 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -1019,7 +1019,7 @@ void GameEngine::update() } #ifdef SAGE_USE_NGMP - NGMP_OnlineServicesManager::getInstance().pollEvents(); + NGMP_OnlineServicesManager::getInstance().update(); #endif } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp index cec8cf4fc06..f341bacd358 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp @@ -27,14 +27,14 @@ void NGMP_OnlineServicesManager::postEvent(const NGMPEvent& event) { m_eventQueue.push(event); } -std::vector NGMP_OnlineServicesManager::pollEvents() { +void NGMP_OnlineServicesManager::update() { std::queue pendingEvents; { std::lock_guard lock(m_eventMutex); std::swap(pendingEvents, m_eventQueue); } - std::vector events; + std::vector uiEvents; while (!pendingEvents.empty()) { NGMPEvent ev = pendingEvents.front(); pendingEvents.pop(); @@ -84,7 +84,7 @@ std::vector NGMP_OnlineServicesManager::pollEvents() { NGMPEvent chatEv; chatEv.type = NGMPEvent::EVENT_CHAT_MESSAGE_RECEIVED; chatEv.payload = msgText; - events.push_back(chatEv); + uiEvents.push_back(chatEv); } else if (msgId == 4) { // NETWORK_ROOM_MEMBER_LIST_UPDATE if (jsonMsg.contains("members") && jsonMsg["members"].is_array()) { @@ -102,7 +102,7 @@ std::vector NGMP_OnlineServicesManager::pollEvents() { } NGMPEvent playersEv; playersEv.type = NGMPEvent::EVENT_PLAYERS_UPDATED; - events.push_back(playersEv); + uiEvents.push_back(playersEv); } } else if (msgId == 7) { // NETWORK_ROOM_LOBBY_LIST_UPDATE @@ -118,9 +118,27 @@ std::vector NGMP_OnlineServicesManager::pollEvents() { break; } if (ev.type != NGMPEvent::EVENT_WEBSOCKET_MESSAGE) { - events.push_back(ev); + uiEvents.push_back(ev); + } + } + + if (!uiEvents.empty()) { + std::lock_guard lock(m_eventMutex); + for (const auto& ev : uiEvents) { + m_uiEventQueue.push(ev); } } +} + +std::vector NGMP_OnlineServicesManager::pollEvents() { + update(); + + std::vector events; + std::lock_guard lock(m_eventMutex); + while (!m_uiEventQueue.empty()) { + events.push_back(m_uiEventQueue.front()); + m_uiEventQueue.pop(); + } return events; } @@ -168,14 +186,16 @@ void NGMP_OnlineServicesManager::requestLobbyListAsync() { if (res == CURLE_OK && httpCode == 200) { try { + fprintf(stderr, "[NGMP-DEBUG] Lobbies JSON: %s\n", response.text.c_str()); + fflush(stderr); auto jsonList = json::parse(response.text); std::vector lobbies; if (jsonList.contains("lobbies") && jsonList["lobbies"].is_array()) { for (const auto& item : jsonList["lobbies"]) { NGMPLobby lobby; lobby.id = item.value("lobbyID", 0LL); - lobby.name = item.value("name", "Custom Lobby"); - lobby.mapName = item.value("map_name", "Tournament Desert"); + lobby.name = item.contains("Name") ? item.value("Name", "Custom Lobby") : item.value("name", "Custom Lobby"); + lobby.mapName = item.contains("MapName") ? item.value("MapName", "Tournament Desert") : item.value("map_name", "Tournament Desert"); lobby.currentPlayers = item.value("current_players", 1); lobby.maxPlayers = item.value("max_players", 8); lobby.hasPassword = item.value("has_password", false); @@ -224,15 +244,13 @@ void NGMP_OnlineServicesManager::createLobbyAsync(const std::string& name, const {"track_stats", false}, {"starting_cash", 10000}, {"passworded", !password.empty()}, + {"password", password}, {"allow_observers", true}, {"max_cam_height", 300}, {"exe_crc", 0}, {"ini_crc", 0}, {"anticheat_id", 0} }; - if (!password.empty()) { - payload["password"] = password; - } std::string payloadStr = payload.dump(); struct curl_slist* headers = nullptr; @@ -255,13 +273,13 @@ void NGMP_OnlineServicesManager::createLobbyAsync(const std::string& name, const curl_slist_free_all(headers); curl_easy_cleanup(curl); - if (res == CURLE_OK && httpCode == 200) { + if (res == CURLE_OK && (httpCode == 200 || httpCode == 201)) { requestLobbyListAsync(); NGMPEvent ev; ev.type = NGMPEvent::EVENT_LOBBY_CREATED; postEvent(ev); } else { - fprintf(stderr, "[NGMP] Create lobby failed (curl=%d, http=%ld)\n", res, httpCode); + fprintf(stderr, "[NGMP] Create lobby failed (curl=%d, http=%ld, resp=%s)\n", res, httpCode, response.text.c_str()); fflush(stderr); } }).detach(); @@ -278,11 +296,9 @@ void NGMP_OnlineServicesManager::joinLobbyAsync(int64_t lobbyId, const std::stri json payload = { {"preferred_port", 0}, {"anticheat_id", 0}, - {"has_map", true} + {"has_map", true}, + {"password", password} }; - if (!password.empty()) { - payload["password"] = password; - } std::string payloadStr = payload.dump(); struct curl_slist* headers = nullptr; @@ -305,13 +321,13 @@ void NGMP_OnlineServicesManager::joinLobbyAsync(int64_t lobbyId, const std::stri curl_slist_free_all(headers); curl_easy_cleanup(curl); - if (res == CURLE_OK && httpCode == 200) { + if (res == CURLE_OK && (httpCode == 200 || httpCode == 201)) { requestLobbyListAsync(); NGMPEvent ev; ev.type = NGMPEvent::EVENT_LOBBY_JOINED; postEvent(ev); } else { - fprintf(stderr, "[NGMP] Join lobby failed (curl=%d, http=%ld)\n", res, httpCode); + fprintf(stderr, "[NGMP] Join lobby failed (curl=%d, http=%ld, resp=%s)\n", res, httpCode, response.text.c_str()); fflush(stderr); } }).detach(); @@ -510,13 +526,20 @@ bool NGMP_OnlineServicesManager::sendChatMessage(const std::string& room, const void NGMP_OnlineServicesManager::changeNetworkRoom(int16_t roomID) { if (!m_isLoggedIn) { + fprintf(stderr, "[NGMP] changeNetworkRoom(%d) ignored: not logged in\n", roomID); + fflush(stderr); return; } - if (m_chatSession) { + if (m_chatSession && m_chatSession->isConnected()) { + fprintf(stderr, "[NGMP] changeNetworkRoom(%d): sending msg_id=3 (room=%d)\n", roomID, roomID); + fflush(stderr); nlohmann::json payload = { {"msg_id", 3}, // NETWORK_ROOM_CHANGE_ROOM {"room", roomID} }; m_chatSession->sendPayload(payload.dump()); + } else { + fprintf(stderr, "[NGMP] changeNetworkRoom(%d): WS chat session not active or not connected\n", roomID); + fflush(stderr); } } From ebd949f7e63633cb9dd3c054727aaa5efd722a20 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 12 Aug 2026 23:41:38 -0300 Subject: [PATCH 21/51] fix(ngmp): populate player slot identity and fix Setup Menu Back button GameSpy slots require an explicitly set identity (TheGameSpyInfo->setLocalName) which was previously missing in the NGMP login flow, causing the host slot to be blank. Also fixed the Back button in WOLGameSetupMenu doing nothing because it incorrectly relied on checking if the GameSpy P2P peer socket was connected before popping the screen. Now it unconditionally pops and calls NGMP changeNetworkRoom(0) to leave the lobby. --- .../GameNetwork/GeneralsOnline/OnlineServices_Manager.h | 2 ++ .../GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp | 6 ++++++ .../GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp | 7 +++++++ .../GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp | 1 + 4 files changed, 16 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h index af4e4c2feae..a8b0a77efb4 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Manager.h @@ -135,6 +135,7 @@ class NGMP_OnlineServicesManager { bool isLoggedIn() const { return m_isLoggedIn; } std::string getAuthToken() const { return m_authToken; } std::string getUsername() const { return m_username; } + int64_t getUserId() const { return m_userId; } const std::vector& getLobbies() const { return m_lobbies; } const std::vector& getLobbyPlayers() const { return m_lobbyPlayers; } @@ -151,6 +152,7 @@ class NGMP_OnlineServicesManager { bool m_initialized = false; bool m_isLoggedIn = false; std::string m_username; + int64_t m_userId = 0; std::string m_authToken; std::string m_wsUri; std::vector m_lobbies; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp index e22e6d08af0..f3e43ad1841 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -65,6 +65,7 @@ #include "GameNetwork/NAT.h" #include "GameNetwork/GUIUtil.h" #include "GameNetwork/GameSpy/GSConfig.h" +#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" void WOLDisplaySlotList(); @@ -1519,6 +1520,10 @@ static void shutdownComplete( WindowLayout *layout ) if (nextScreen != nullptr) { +#if defined(SAGE_USE_NGMP) + NGMP_OnlineServicesManager::getInstance().changeNetworkRoom(0); + TheShell->push(nextScreen); +#else if (!TheGameSpyPeerMessageQueue || !TheGameSpyPeerMessageQueue->isConnected()) { DEBUG_LOG(("GameSetup shutdownComplete() - skipping push because we're disconnected")); @@ -1527,6 +1532,7 @@ static void shutdownComplete( WindowLayout *layout ) { TheShell->push(nextScreen); } +#endif } /* diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp index 032886cd2bd..aee99ace27d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLoginMenu.cpp @@ -824,6 +824,13 @@ void WOLLoginMenuUpdate( WindowLayout * layout, void *userData) { if (!buttonPushed) { + // GeneralsX @feature fbraz3 12/08/2026 Populate GameSpyInfo with NGMP identity so player slots and chat work + std::string username = NGMP_OnlineServicesManager::getInstance().getUsername(); + int64_t userId = NGMP_OnlineServicesManager::getInstance().getUserId(); + AsciiString aName(username.c_str()); + TheGameSpyInfo->setLocalName(aName); + TheGameSpyInfo->setLocalProfileID(static_cast(userId)); + buttonPushed = true; loginAttemptTime = 0; nextScreen = "Menus/WOLWelcomeMenu.wnd"; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp index c1a03253807..cd67215ebc6 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp @@ -137,6 +137,7 @@ void NGMP_OnlineServicesManager::beginBrowserLogin() { m_authToken = sessionToken; m_username = displayName; + m_userId = userId; m_wsUri = wsUri; m_isLoggedIn = true; From 80e4336482c3e9f4816382275f1a737f7539ecfd Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 12 Aug 2026 23:54:10 -0300 Subject: [PATCH 22/51] fix(ngmp): support pascal case json keys for lobby list parsing --- .../GameNetwork/GeneralsOnline/NGMPWebSocket.h | 2 ++ .../GeneralsOnline/OnlineServices_WebSocket.cpp | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPWebSocket.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPWebSocket.h index 9a1eaf38329..578e2cfef41 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPWebSocket.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMPWebSocket.h @@ -15,6 +15,7 @@ #include #include #include +#include #pragma pop_macro("max") #pragma pop_macro("min") @@ -91,6 +92,7 @@ class NGMPWebSocket { std::thread m_recvThread; std::atomic m_running = false; GenericMessageCallback m_messageCallback; + mutable std::mutex m_sendMutex; }; } // namespace NGMP diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_WebSocket.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_WebSocket.cpp index cd11907ed04..a2a409313a8 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_WebSocket.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_WebSocket.cpp @@ -64,6 +64,11 @@ bool NGMPWebSocket::connect(const std::string& wsUrl, const std::string& authTok void NGMPWebSocket::disconnect() { m_running = false; + if (m_curl) { + std::lock_guard lock(m_sendMutex); + size_t sent = 0; + curl_ws_send(m_curl, "", 0, &sent, 0, CURLWS_CLOSE); + } if (m_recvThread.joinable()) { m_recvThread.join(); } @@ -77,16 +82,21 @@ void NGMPWebSocket::disconnect() { bool NGMPWebSocket::sendPayload(const std::string& payload) { if (!m_running.load() || !m_curl) { + fprintf(stderr, "[NGMP-WebSocket] Cannot send payload, WS not running or null curl (running=%d)\n", m_running.load()); + fflush(stderr); return false; } + std::lock_guard lock(m_sendMutex); size_t sent = 0; CURLcode res = curl_ws_send(m_curl, payload.c_str(), payload.size(), &sent, 0, CURLWS_TEXT); if (res != CURLE_OK) { - fprintf(stderr, "[NGMP-WebSocket] Failed to send WS payload: %s\n", curl_easy_strerror(res)); + fprintf(stderr, "[NGMP-WebSocket] Failed to send WS payload (%s): %s\n", payload.c_str(), curl_easy_strerror(res)); fflush(stderr); return false; } + fprintf(stderr, "[NGMP-WebSocket] Sent WS payload: %s\n", payload.c_str()); + fflush(stderr); return true; } @@ -100,8 +110,9 @@ void NGMPWebSocket::receiveLoop() { auto now = std::chrono::steady_clock::now(); if (std::chrono::duration_cast(now - lastPingTime).count() >= 10) { lastPingTime = now; - std::string pingPayload = "{\"msg_id\":8}"; + std::string pingPayload = "{\"msg_id\":14}"; // EWebSocketMessageID::PING = 14 size_t sent = 0; + std::lock_guard lock(m_sendMutex); curl_ws_send(m_curl, pingPayload.c_str(), pingPayload.size(), &sent, 0, CURLWS_TEXT); } From 0c7527b18b31b21dc64bcb59a78221c259f535ba Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Thu, 13 Aug 2026 23:32:04 -0300 Subject: [PATCH 23/51] fix(ngmp): synchronize curl_ws_recv to prevent libcurl crash libcurl does not support concurrent access to the same CURL handle. When the main thread called curl_ws_send or curl_easy_cleanup while the receive thread was running curl_ws_recv, the allocator corrupted and crashed the game with SIGABRT (malloc bug pointer being freed was not allocated). Now both receive and send are synchronized over m_sendMutex. --- .../GeneralsOnline/OnlineServices_WebSocket.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_WebSocket.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_WebSocket.cpp index a2a409313a8..1ab9a790653 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_WebSocket.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_WebSocket.cpp @@ -117,7 +117,13 @@ void NGMPWebSocket::receiveLoop() { } size_t received = 0; - CURLcode res = curl_ws_recv(m_curl, buffer, sizeof(buffer) - 1, &received, &meta); + CURLcode res; + + { + std::lock_guard lock(m_sendMutex); + if (!m_curl) break; // In case we disconnected + res = curl_ws_recv(m_curl, buffer, sizeof(buffer) - 1, &received, &meta); + } if (res == CURLE_AGAIN) { // No data ready — yield briefly to avoid busy-spinning From 364381a1331c46575e4e1eb9bcd56fe1450e9c9b Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Thu, 13 Aug 2026 23:54:12 -0300 Subject: [PATCH 24/51] fix(ngmp): populate player identity, hide local ping, and return to lobby 1. Populate TheGameSpyInfo localName and profileID upon NGMP login in OnlineServices_Manager and MainMenuUpdate so the host player name displays properly in room slots. 2. Hide ping indicator for the local player slot in WOLGameSetupMenu, following references/GameClient pattern. 3. Push WOLCustomLobby.wnd upon game completion in WOLGameSetupMenuInit for NGMP builds. --- .../GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp | 6 ++++++ .../GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp | 10 ++++++++++ .../GeneralsOnline/OnlineServices_Manager.cpp | 7 ++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp index a0c252a8443..7099ad6f21d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/MainMenu.cpp @@ -876,6 +876,12 @@ void MainMenuUpdate( WindowLayout *layout, void *userData ) if (buttonPushed) { buttonPushed = FALSE; dontAllowTransitions = FALSE; + std::string username = NGMP_OnlineServicesManager::getInstance().getUsername(); + int64_t userId = NGMP_OnlineServicesManager::getInstance().getUserId(); + if (TheGameSpyInfo) { + TheGameSpyInfo->setLocalName(AsciiString(username.c_str())); + TheGameSpyInfo->setLocalProfileID(static_cast(userId)); + } TheShell->push("Menus/WOLWelcomeMenu.wnd"); } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp index f3e43ad1841..e31c799cf05 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -1064,6 +1064,12 @@ void WOLDisplaySlotList() { // set up my own ping... slot->setPingString(TheGameSpyInfo->getPingString()); +#if defined(SAGE_USE_NGMP) + // References: GameClient hides the ping window for local player + if (genericPingWindow[i]) + genericPingWindow[i]->winHide(TRUE); + continue; +#endif } if (genericPingWindow[i]) @@ -1341,11 +1347,15 @@ void WOLGameSetupMenuInit( WindowLayout *layout, void *userData ) // after the game. So, we pop the menu and go back to the lobby. Whee! DEBUG_LOG(("WOLGameSetupMenuInit() - game was in progress, so pop immediate back to lobby")); TheShell->popImmediate(); +#if defined(SAGE_USE_NGMP) + TheShell->push("Menus/WOLCustomLobby.wnd", TRUE); +#else if (TheGameSpyPeerMessageQueue && TheGameSpyPeerMessageQueue->isConnected()) { DEBUG_LOG(("We're still connected, so pushing back on the lobby")); TheShell->push("Menus/WOLCustomLobby.wnd", TRUE); } +#endif return; } TheGameSpyInfo->setCurrentGroupRoom(0); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp index f341bacd358..bfaaaa82bf3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp @@ -5,6 +5,7 @@ #include "GameNetwork/GeneralsOnline/NGMP_Helpers.h" #include "GameNetwork/GeneralsOnline/ngmp_curl_utils.h" #include "GameNetwork/GeneralsOnline/NGMPWebSocket.h" +#include "GameNetwork/GameSpy/PeerDefs.h" #include #include #include @@ -41,8 +42,12 @@ void NGMP_OnlineServicesManager::update() { switch (ev.type) { case NGMPEvent::EVENT_AUTH_SUCCESS: - fprintf(stderr, "[NGMP-MainThread] Event: Auth Success\n"); + fprintf(stderr, "[NGMP-MainThread] Event: Auth Success (user=%s id=%lld)\n", m_username.c_str(), (long long)m_userId); m_isLoggedIn = true; + if (TheGameSpyInfo) { + TheGameSpyInfo->setLocalName(AsciiString(m_username.c_str())); + TheGameSpyInfo->setLocalProfileID(static_cast(m_userId)); + } if (!m_chatSession) { m_chatSession.reset(new NGMP::NGMPWebSocket()); m_chatSession->setMessageCallback([this](const std::string& rawJson) { From b1b7f22ec325fde70c093914a04da067423e657c Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Thu, 13 Aug 2026 23:54:58 -0300 Subject: [PATCH 25/51] docs(worklog): update 2026-08-DIARY with NGMP libcurl and lobby UX fixes --- docs/WORKLOG/2026-08-DIARY.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/WORKLOG/2026-08-DIARY.md b/docs/WORKLOG/2026-08-DIARY.md index d4986eec5f2..92952389aba 100644 --- a/docs/WORKLOG/2026-08-DIARY.md +++ b/docs/WORKLOG/2026-08-DIARY.md @@ -50,3 +50,12 @@ - `WOLLobbyMenu.cpp`: Moved `markAsStagingRoomHost()` / `markAsStagingRoomJoiner()` to execute **before** `TheShell->pop()` in the `EVENT_LOBBY_CREATED` and `EVENT_LOBBY_JOINED` handler. - `WOLGameSetupMenu.cpp`: Added defensive null-check for `getCurrentStagingRoom()` return value to prevent future crashes if state is somehow lost; logs to stderr and pops back to lobby. - `PopupHostGame.cpp`: Added diagnostic `fprintf(stderr, ...)` log before `createLobbyAsync` call to trace invocation. + +## 2026-08-13 + +### NGMP: WebSocket libcurl concurrency fix & Lobby UX Polish + +- **WebSocket Thread Safety**: Fixed SIGABRT malloc crash (`___BUG_IN_CLIENT_OF_LIBMALLOC_POINTER_BEING_FREED_WAS_NOT_ALLOCATED`) in `OnlineServices_WebSocket.cpp` by wrapping `curl_ws_recv` with `m_sendMutex`. `libcurl` handles are strictly non-thread-safe, so synchronizing recv/send/disconnect prevents race conditions. +- **Player Identity in Staging Room**: Added `TheGameSpyInfo->setLocalName` and `setLocalProfileID` calls upon `EVENT_AUTH_SUCCESS` in `OnlineServices_Manager.cpp` and `MainMenu.cpp`, ensuring slot 0 displays the logged-in username (`DEV_ACCOUNT_0`). +- **Ping Indicator Visibility**: Following `references/GameClient`, hidden `genericPingWindow` for local player slots in `WOLGameSetupMenu.cpp`. +- **Post-Match Return Flow**: Fixed return transition in `WOLGameSetupMenuInit` after game end under `SAGE_USE_NGMP` to correctly push `Menus/WOLCustomLobby.wnd` instead of falling back to Main Menu. From 33c77511191b6cdf2f27239db5e08e032f6e29f3 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Fri, 14 Aug 2026 15:23:03 -0300 Subject: [PATCH 26/51] feat(ngmp): populate lobby roster and restore online welcome voice line - Populate custom lobby player listbox from NGMP lobby players with rank icons - Fix chat message JSON payload schema and listbox routing - Restore classic Welcome to Generals Online voice line on login - Gate voice line playback to once per session with transition safety guards - Backport welcome menu improvements to Generals base game - Update August 2026 worklog --- .../GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp | 22 +++ .../GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp | 139 ++++++++++++------ .../GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp | 23 +++ .../GeneralsOnline/OnlineServices_Manager.cpp | 48 +++++- docs/WORKLOG/2026-08-DIARY.md | 11 ++ 5 files changed, 193 insertions(+), 50 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp index 403d17c9bea..2418d446b1c 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp @@ -34,6 +34,8 @@ #include "gamespy/peer/peer.h" #include "Common/GameEngine.h" +#include "Common/AudioEventRTS.h" +#include "Common/GameAudio.h" #include "Common/GameSpyMiscPreferences.h" #include "Common/CustomMatchPreferences.h" #include "Common/GlobalData.h" @@ -109,6 +111,7 @@ static GameWindow *staticTextHighscoreRank = nullptr; static GameWindow *staticTextHighscorePoints = nullptr; static UnicodeString gServerName; +static Bool s_welcomeAudioPlayed = FALSE; void updateServerDisplay(UnicodeString serverName) { if (staticTextServerName) @@ -197,6 +200,10 @@ static void shutdownComplete( WindowLayout *layout ) { TheShell->push(nextScreen); } + else + { + s_welcomeAudioPlayed = FALSE; + } nextScreen = nullptr; @@ -541,6 +548,13 @@ void WOLWelcomeMenuInit( WindowLayout *layout, void *userData ) raiseMessageBoxes = TRUE; TheTransitionHandler->setGroup("WOLWelcomeMenuFade"); + // GeneralsX @feature Play classic "Welcome to Generals Online" voice line once per login session + if (!s_welcomeAudioPlayed && !GameSpyIsOverlayOpen(GSOVERLAY_LOCALESELECT) && TheAudio) + { + AudioEventRTS welcomeSound("WelcomeToGeneralsOnline"); + TheAudio->addAudioEvent(&welcomeSound); + s_welcomeAudioPlayed = TRUE; + } } //------------------------------------------------------------------------------------------------- @@ -604,6 +618,14 @@ void WOLWelcomeMenuUpdate( WindowLayout * layout, void *userData) } } + // GeneralsX @feature Play classic "Welcome to Generals Online" once locale overlay is closed + if (!isShuttingDown && !buttonPushed && !s_welcomeAudioPlayed && !GameSpyIsOverlayOpen(GSOVERLAY_LOCALESELECT) && TheAudio) + { + AudioEventRTS welcomeSound("WelcomeToGeneralsOnline"); + TheAudio->addAudioEvent(&welcomeSound); + s_welcomeAudioPlayed = TRUE; + } + if (TheShell->isAnimFinished() && !buttonPushed && TheGameSpyPeerMessageQueue) { HandleBuddyResponses(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp index 8bb8a3967e3..b2dd54ab99e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp @@ -441,62 +441,98 @@ static Int insertPlayerInListbox(const PlayerInfo& info, Color color) Int currentRank = info.m_rankPoints; Int currentSide = info.m_side; - /* since PersistentStorage updates now update PlayerInfo, we don't need this. - if (info.m_profileID) - { - PSPlayerStats psStats = TheGameSpyPSMessageQueue->findPlayerStatsByID(info.m_profileID); - if (psStats.id) - { - currentRank = CalculateRank(psStats); - PerGeneralMap::iterator it; - Int numGames = 0; - for(it = psStats.games.begin(); it != psStats.games.end(); ++it) - { - if(it->second >= numGames) - { - numGames = it->second; - currentSide = it->first; - } - } - if(numGames == 0 || psStats.gamesAsRandom >= numGames ) - { - currentSide = 0; - } - } + Int w = 10; + if (listboxLobbyPlayers) + { + Int colW = GadgetListBoxGetColumnWidth(listboxLobbyPlayers, 0); + if (colW > 0) + w = colW; } - */ - - Bool isPreorder = TheGameSpyInfo->didPlayerPreorder(info.m_profileID); - - const Image *preorderImg = TheMappedImageCollection->findImageByName("OfficersClubsmall"); - Int w = (preorderImg)?preorderImg->getImageWidth():10; - //Int h = (preorderImg)?preorderImg->getImageHeight():10; - w = min(GadgetListBoxGetColumnWidth(listboxLobbyPlayers, 0), w); Int h = w; - if (!isPreorder) - preorderImg = nullptr; const Image *rankImg = LookupSmallRankImage(currentSide, currentRank); -#if 0 //Officer's Club (preorder image) no longer used in Zero Hour - Int index = GadgetListBoxAddEntryImage(listboxLobbyPlayers, preorderImg, -1, 0, w, h); - GadgetListBoxAddEntryImage(listboxLobbyPlayers, rankImg, index, 1, w, h); - GadgetListBoxAddEntryText(listboxLobbyPlayers, uStr, color, index, 2); -#else Int index = GadgetListBoxAddEntryImage(listboxLobbyPlayers, rankImg, -1, 0, w, h); GadgetListBoxAddEntryText(listboxLobbyPlayers, uStr, color, index, 1); -#endif + GadgetListBoxSetItemData(listboxLobbyPlayers, (void*)(intptr_t)info.m_profileID, index); return index; } void PopulateLobbyPlayerListbox() { - if (!listboxLobbyPlayers) return; +#if defined(SAGE_USE_NGMP) + // GeneralsX @feature GeneralsOnline Populate player roster in custom lobby + Int maxSelectedItems = GadgetListBoxGetNumEntries(listboxLobbyPlayers); + Int *selectedIndices = nullptr; + GadgetListBoxGetSelected(listboxLobbyPlayers, (Int *)(&selectedIndices)); + std::set selectedNames; + UnicodeString uStr; + Int numSelected = 0; + for (Int i = 0; i < maxSelectedItems; ++i) + { + if (!selectedIndices || selectedIndices[i] < 0) + break; + ++numSelected; + AsciiString selectedName; + uStr = GadgetListBoxGetText(listboxLobbyPlayers, selectedIndices[i], COLUMN_PLAYERNAME); + selectedName.translate(uStr); + selectedNames.insert(selectedName); + } + + Int previousTopIndex = GadgetListBoxGetTopVisibleEntry(listboxLobbyPlayers); + GadgetListBoxReset(listboxLobbyPlayers); + + std::set indicesToSelect; + auto lobbyPlayers = NGMP_OnlineServicesManager::getInstance().getLobbyPlayers(); + + // Fallback: If roster from server is not populated yet, show local authenticated user + if (lobbyPlayers.empty()) { + AsciiString localName = TheGameSpyInfo ? TheGameSpyInfo->getLocalName() : AsciiString::TheEmptyString; + if (!localName.isEmpty()) { + NGMPLobbyPlayer lp; + lp.id = TheGameSpyInfo->getLocalProfileID(); + lp.name = localName.str(); + lp.isAdmin = false; + lobbyPlayers.push_back(lp); + } + } + + for (const auto& p : lobbyPlayers) + { + PlayerInfo info; + info.m_name = p.name.c_str(); + info.m_profileID = static_cast(p.id); + info.m_flags = p.isAdmin ? PEER_FLAG_OP : 0; + Color color = p.isAdmin ? GameSpyColor[GSCOLOR_PLAYER_OWNER] : GameSpyColor[GSCOLOR_PLAYER_NORMAL]; + Int index = insertPlayerInListbox(info, color); + + if (selectedNames.find(info.m_name) != selectedNames.end()) + { + indicesToSelect.insert(index); + } + } + + if (!indicesToSelect.empty()) + { + const size_t count = indicesToSelect.size(); + size_t index = 0; + Int *newIndices = NEW Int[count]; + for (auto idx : indicesToSelect) + { + newIndices[index++] = idx; + } + GadgetListBoxSetSelected(listboxLobbyPlayers, newIndices, count); + delete[] newIndices; + } + + GadgetListBoxSetTopVisibleEntry(listboxLobbyPlayers, previousTopIndex); + return; +#else // Display players PlayerInfoMap *players = TheGameSpyInfo->getPlayerInfoMap(); PlayerInfoMap::iterator it; @@ -537,7 +573,7 @@ void PopulateLobbyPlayerListbox() for (it = players->begin(); it != players->end(); ++it) { PlayerInfo info = it->second; - if (info.m_flags & PEER_FLAG_OP || TheGameSpyConfig->isPlayerVIP(info.m_profileID)) + if (info.m_flags & PEER_FLAG_OP || (TheGameSpyConfig && TheGameSpyConfig->isPlayerVIP(info.m_profileID))) { Int index = insertPlayerInListbox(info, info.isIgnored()?GameSpyColor[GSCOLOR_PLAYER_IGNORED]:GameSpyColor[GSCOLOR_PLAYER_OWNER]); @@ -555,7 +591,7 @@ void PopulateLobbyPlayerListbox() { PlayerInfo info = it->second; bIt = buddies->find(info.m_profileID); - if ( !(info.m_flags & PEER_FLAG_OP || TheGameSpyConfig->isPlayerVIP(info.m_profileID)) && bIt != buddies->end() ) + if ( !(info.m_flags & PEER_FLAG_OP || (TheGameSpyConfig && TheGameSpyConfig->isPlayerVIP(info.m_profileID))) && bIt != buddies->end() ) { Int index = insertPlayerInListbox(info, info.isIgnored()?GameSpyColor[GSCOLOR_PLAYER_IGNORED]:GameSpyColor[GSCOLOR_PLAYER_BUDDY]); @@ -573,7 +609,7 @@ void PopulateLobbyPlayerListbox() { PlayerInfo info = it->second; bIt = buddies->find(info.m_profileID); - if ( !(info.m_flags & PEER_FLAG_OP || TheGameSpyConfig->isPlayerVIP(info.m_profileID)) && bIt == buddies->end() ) + if ( !(info.m_flags & PEER_FLAG_OP || (TheGameSpyConfig && TheGameSpyConfig->isPlayerVIP(info.m_profileID))) && bIt == buddies->end() ) { Int index = insertPlayerInListbox(info, info.isIgnored()?GameSpyColor[GSCOLOR_PLAYER_IGNORED]:GameSpyColor[GSCOLOR_PLAYER_NORMAL]); @@ -612,7 +648,7 @@ void PopulateLobbyPlayerListbox() // restore top visible entry GadgetListBoxSetTopVisibleEntry(listboxLobbyPlayers, previousTopIndex); } - +#endif } //------------------------------------------------------------------------------------------------- @@ -741,6 +777,7 @@ void WOLLobbyMenuInit( WindowLayout *layout, void *userData ) // GeneralsX @feature GeneralsOnline Enter global lobby room 0; block lobby refresh until server confirms room change ngmpRoomConfirmed = FALSE; NGMP_OnlineServicesManager::getInstance().changeNetworkRoom(0); + PopulateLobbyPlayerListbox(); #endif } @@ -967,7 +1004,12 @@ void WOLLobbyMenuUpdate( WindowLayout * layout, void *userData) AsciiString msg(ev.payload.c_str()); UnicodeString uMsg; uMsg.translate(msg); - TheGameSpyInfo->addText(uMsg, GameSpyColor[GSCOLOR_DEFAULT], nullptr); + if (listboxLobbyChat) { + Int index = GadgetListBoxAddEntryText(listboxLobbyChat, uMsg, GameSpyColor[GSCOLOR_DEFAULT], -1, -1); + GadgetListBoxSetItemData(listboxLobbyChat, (void*)-1, index); + } else { + TheGameSpyInfo->addText(uMsg, GameSpyColor[GSCOLOR_DEFAULT], nullptr); + } } else if (ev.type == NGMPEvent::EVENT_PLAYERS_UPDATED) { TheGameSpyInfo->getPlayerInfoMap()->clear(); @@ -1742,7 +1784,16 @@ WindowMsgHandledType WOLLobbyMenuSystem( GameWindow *window, UnsignedInt msg, if (!txtInput.isEmpty()) { // Send the message +#if defined(SAGE_USE_NGMP) + if (!handleLobbySlashCommands(txtInput)) + { + AsciiString msg; + msg.translate(txtInput); + NGMP_OnlineServicesManager::getInstance().sendChatMessage("lobby", msg.str()); + } +#else TheGameSpyInfo->sendChat( txtInput, FALSE, listboxLobbyPlayers ); // 'emote' button now just sends text +#endif } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp index 402626e1d95..d6e5a591997 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp @@ -34,6 +34,8 @@ #include "gamespy/peer/peer.h" #include "Common/GameEngine.h" +#include "Common/AudioEventRTS.h" +#include "Common/GameAudio.h" #include "Common/GameSpyMiscPreferences.h" #include "Common/CustomMatchPreferences.h" #include "Common/GlobalData.h" @@ -132,6 +134,7 @@ static GameWindow *staticTextHighscoreRank = nullptr; static GameWindow *staticTextHighscorePoints = nullptr; static UnicodeString gServerName; +static Bool s_welcomeAudioPlayed = FALSE; void updateServerDisplay(UnicodeString serverName) { if (staticTextServerName) @@ -220,6 +223,10 @@ static void shutdownComplete( WindowLayout *layout ) { TheShell->push(nextScreen); } + else + { + s_welcomeAudioPlayed = FALSE; + } nextScreen = nullptr; @@ -668,6 +675,14 @@ void WOLWelcomeMenuInit( WindowLayout *layout, void *userData ) raiseMessageBoxes = TRUE; TheTransitionHandler->setGroup("WOLWelcomeMenuFade"); + // GeneralsX @feature Play classic "Welcome to Generals Online" voice line once per login session + if (!s_welcomeAudioPlayed && !GameSpyIsOverlayOpen(GSOVERLAY_LOCALESELECT) && TheAudio) + { + AudioEventRTS welcomeSound("WelcomeToGeneralsOnline"); + TheAudio->addAudioEvent(&welcomeSound); + s_welcomeAudioPlayed = TRUE; + } + fprintf(stderr, "[WOLWelcomeMenuInit] Done.\n"); fflush(stderr); } @@ -741,6 +756,14 @@ void WOLWelcomeMenuUpdate( WindowLayout * layout, void *userData) } #endif + // GeneralsX @feature Play classic "Welcome to Generals Online" once locale overlay is closed + if (!isShuttingDown && !buttonPushed && !s_welcomeAudioPlayed && !GameSpyIsOverlayOpen(GSOVERLAY_LOCALESELECT) && TheAudio) + { + AudioEventRTS welcomeSound("WelcomeToGeneralsOnline"); + TheAudio->addAudioEvent(&welcomeSound); + s_welcomeAudioPlayed = TRUE; + } + if (TheShell->isAnimFinished() && !buttonPushed && TheGameSpyPeerMessageQueue) { HandleBuddyResponses(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp index bfaaaa82bf3..7f95e04e833 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Manager.cpp @@ -77,6 +77,8 @@ void NGMP_OnlineServicesManager::update() { case NGMPEvent::EVENT_WEBSOCKET_MESSAGE: { try { + fprintf(stderr, "[NGMP] EVENT_WEBSOCKET_MESSAGE raw: %s\n", ev.payload.c_str()); + fflush(stderr); auto jsonMsg = nlohmann::json::parse(ev.payload); if (jsonMsg.contains("msg_id") && jsonMsg["msg_id"].is_number_integer()) { int msgId = jsonMsg["msg_id"].get(); @@ -96,15 +98,41 @@ void NGMP_OnlineServicesManager::update() { std::vector updatedPlayers; for (const auto& member : jsonMsg["members"]) { NGMPLobbyPlayer player; - player.id = member.value("UserID", 0LL); - player.name = member.value("Name", ""); - player.isAdmin = member.value("IsAdmin", false); + if (member.contains("UserID") && !member["UserID"].is_null()) { + if (member["UserID"].is_number()) player.id = member["UserID"].get(); + else if (member["UserID"].is_string()) player.id = std::stoll(member["UserID"].get()); + } else if (member.contains("user_id") && !member["user_id"].is_null()) { + if (member["user_id"].is_number()) player.id = member["user_id"].get(); + else if (member["user_id"].is_string()) player.id = std::stoll(member["user_id"].get()); + } + + if (member.contains("Name") && member["Name"].is_string()) { + player.name = member["Name"].get(); + } else if (member.contains("name") && member["name"].is_string()) { + player.name = member["name"].get(); + } else if (member.contains("display_name") && member["display_name"].is_string()) { + player.name = member["display_name"].get(); + } else if (member.contains("DisplayName") && member["DisplayName"].is_string()) { + player.name = member["DisplayName"].get(); + } + + if (member.contains("IsAdmin") && member["IsAdmin"].is_boolean()) { + player.isAdmin = member["IsAdmin"].get(); + } else if (member.contains("is_admin") && member["is_admin"].is_boolean()) { + player.isAdmin = member["is_admin"].get(); + } updatedPlayers.push_back(player); } { std::lock_guard lock(m_eventMutex); m_lobbyPlayers = std::move(updatedPlayers); } + fprintf(stderr, "[NGMP] Updated lobby player roster (%zu players)\n", m_lobbyPlayers.size()); + for (const auto& p : m_lobbyPlayers) { + fprintf(stderr, " [LobbyPlayer] id=%lld, name='%s', isAdmin=%d\n", (long long)p.id, p.name.c_str(), p.isAdmin); + } + fflush(stderr); + NGMPEvent playersEv; playersEv.type = NGMPEvent::EVENT_PLAYERS_UPDATED; uiEvents.push_back(playersEv); @@ -114,8 +142,12 @@ void NGMP_OnlineServicesManager::update() { requestLobbyListAsync(); } } + } catch (const std::exception& e) { + fprintf(stderr, "[NGMP] Failed to parse WS message (%s): %s\n", e.what(), ev.payload.c_str()); + fflush(stderr); } catch (...) { fprintf(stderr, "[NGMP] Failed to parse WS message: %s\n", ev.payload.c_str()); + fflush(stderr); } } break; @@ -513,15 +545,19 @@ GlobalStats NGMP_OnlineServicesManager::getGlobalStats() const { bool NGMP_OnlineServicesManager::sendChatMessage(const std::string& room, const std::string& message) { if (!m_isLoggedIn) { + fprintf(stderr, "[NGMP] sendChatMessage ignored: not logged in\n"); + fflush(stderr); return false; } - if (m_chatSession) { - // Build the chat message payload + if (m_chatSession && m_chatSession->isConnected()) { + // Build the chat message payload for NETWORK_ROOM_CHAT_FROM_CLIENT (msg_id: 1) nlohmann::json payload = { {"msg_id", 1}, - {"action", "chat"}, + {"action", false}, {"message", message} }; + fprintf(stderr, "[NGMP] sendChatMessage: sending '%s'\n", message.c_str()); + fflush(stderr); return m_chatSession->sendPayload(payload.dump()); } fprintf(stderr, "[NGMP] sendChatMessage called but no active chat session\n"); diff --git a/docs/WORKLOG/2026-08-DIARY.md b/docs/WORKLOG/2026-08-DIARY.md index 92952389aba..fd1efcb201d 100644 --- a/docs/WORKLOG/2026-08-DIARY.md +++ b/docs/WORKLOG/2026-08-DIARY.md @@ -59,3 +59,14 @@ - **Player Identity in Staging Room**: Added `TheGameSpyInfo->setLocalName` and `setLocalProfileID` calls upon `EVENT_AUTH_SUCCESS` in `OnlineServices_Manager.cpp` and `MainMenu.cpp`, ensuring slot 0 displays the logged-in username (`DEV_ACCOUNT_0`). - **Ping Indicator Visibility**: Following `references/GameClient`, hidden `genericPingWindow` for local player slots in `WOLGameSetupMenu.cpp`. - **Post-Match Return Flow**: Fixed return transition in `WOLGameSetupMenuInit` after game end under `SAGE_USE_NGMP` to correctly push `Menus/WOLCustomLobby.wnd` instead of falling back to Main Menu. + +## 2026-08-14 + +### NGMP: Custom Lobby Player Roster Population & Null Safety + +- **Player Roster in Custom Lobby (`WOLLobbyMenu.cpp`)**: Added `#if defined(SAGE_USE_NGMP)` implementation for `PopulateLobbyPlayerListbox()` that iterates `NGMP_OnlineServicesManager::getLobbyPlayers()`, safely inserting each player and rank image into `ListboxPlayers`. Added immediate call to `PopulateLobbyPlayerListbox()` in `WOLLobbyMenuInit` with a local user fallback. +- **Null Safety in Legacy Code Paths**: Added null pointer check for `TheGameSpyConfig` before invoking `isPlayerVIP` in `WOLLobbyMenu.cpp` to prevent virtual method crashes on uninitialized GameSpy configs. +- **WebSocket JSON Parsing & Diagnostic Logging (`OnlineServices_Manager.cpp`)**: Extended `msg_id: 4` (`NETWORK_ROOM_MEMBER_LIST_UPDATE`) deserialization to gracefully handle varying case conventions (`UserID`/`user_id`, `Name`/`display_name`, `IsAdmin`/`is_admin`) and added real-time diagnostic console logging for incoming WebSocket frames and parsed rosters. +- **Classic Voice Line "Welcome to Generals Online" (`WOLWelcomeMenu.cpp`)**: Added `WelcomeToGeneralsOnline` audio event trigger (`DialogEvent` with `euonline.wav`) upon initializing the online welcome screen (`WOLWelcomeMenuInit`), reproducing the retail Westwood/EA online greeting. Gated playback to execute only once per login session (`s_welcomeAudioPlayed`), deferring playback until after initial region selection (`GSOVERLAY_LOCALESELECT`) closes on first login, guarding against shutdown/exit transitions (`!isShuttingDown && !buttonPushed`), and preventing repetitive audio triggers when returning from submenus (Custom Match, Quick Match, Buddies, Player Info). Reset upon completion of return-to-main-menu shutdown transition (`shutdownComplete`). Backported to both Zero Hour and Generals base game. + + From 751583e59dd581462e033d459f89729e1f4b9047 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Fri, 14 Aug 2026 16:51:50 -0300 Subject: [PATCH 27/51] fix(build): resolve base game inclusion and offline flatpak json dependency - Move RefreshNGMPGameListBoxes from shared Core LobbyUtils to WOLLobbyMenu to decouple base game from Zero Hour NGMP headers - Bundle json.hpp and update NGMP_json.h with fallback header resolution for offline/sandboxed Flatpak environments - Safeguard FetchContent in cmake/ngmp.cmake when FETCHCONTENT_FULLY_DISCONNECTED is enabled - Update August 2026 worklog --- .../Include/GameNetwork/GameSpy/LobbyUtils.h | 1 - .../Source/GameNetwork/GameSpy/LobbyUtils.cpp | 44 - .../GameNetwork/GeneralsOnline/NGMP_json.h | 8 + .../GameNetwork/GeneralsOnline/json.hpp | 25510 ++++++++++++++++ .../GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp | 40 + cmake/ngmp.cmake | 2 +- docs/WORKLOG/2026-08-DIARY.md | 3 + 7 files changed, 25562 insertions(+), 46 deletions(-) create mode 100644 GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/json.hpp diff --git a/Core/GameEngine/Include/GameNetwork/GameSpy/LobbyUtils.h b/Core/GameEngine/Include/GameNetwork/GameSpy/LobbyUtils.h index 0ef9c61b4c6..bb07bc2f899 100644 --- a/Core/GameEngine/Include/GameNetwork/GameSpy/LobbyUtils.h +++ b/Core/GameEngine/Include/GameNetwork/GameSpy/LobbyUtils.h @@ -38,7 +38,6 @@ void GrabWindowInfo(); void ReleaseWindowInfo(); void RefreshGameInfoListBox( GameWindow *mainWin, GameWindow *win ); void RefreshGameListBoxes(); -void RefreshNGMPGameListBoxes(const std::vector& lobbies); void ToggleGameListType(); void playerTemplateComboBoxTooltip(GameWindow *wndComboBox, WinInstanceData *instData, UnsignedInt mouse); diff --git a/Core/GameEngine/Source/GameNetwork/GameSpy/LobbyUtils.cpp b/Core/GameEngine/Source/GameNetwork/GameSpy/LobbyUtils.cpp index b0b689d030b..f8b8448f317 100644 --- a/Core/GameEngine/Source/GameNetwork/GameSpy/LobbyUtils.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameSpy/LobbyUtils.cpp @@ -61,10 +61,6 @@ #include "GameNetwork/GameSpy/PersistentStorageDefs.h" #include "GameNetwork/GameSpy/GSConfig.h" -#if defined(SAGE_USE_NGMP) -#include "GameNetwork/GeneralsOnline/OnlineServices_Manager.h" -#endif - #include "Common/STLTypedefs.h" @@ -870,46 +866,6 @@ void RefreshGameListBoxes() } } -void RefreshNGMPGameListBoxes(const std::vector& lobbies) -{ - GameWindow *win = GetGameListBox(); - if (!win) - return; - - // clear it out - GadgetListBoxReset(win); - - Color gameColor = GameSpyColor[GSCOLOR_GAME]; - - for (size_t i = 0; i < lobbies.size(); ++i) - { - const auto& lobby = lobbies[i]; - AsciiString asciiName(lobby.name.c_str()); - UnicodeString uName; - uName.translate(asciiName); - - Int index = GadgetListBoxAddEntryText(win, uName, gameColor, -1, COLUMN_NAME); - GadgetListBoxSetItemData(win, reinterpret_cast(static_cast(i + 1)), index); - - AsciiString asciiMap(lobby.mapName.c_str()); - UnicodeString uMap; - uMap.translate(asciiMap); - - GadgetListBoxAddEntryText(win, uMap, gameColor, index, COLUMN_MAP); - - // Ladder info usually goes here, but we can just leave it blank for now - GadgetListBoxAddEntryText(win, L" ", gameColor, index, COLUMN_LADDER); - - UnicodeString playersStr; - playersStr.format(L"%d/%d", lobby.currentPlayers, lobby.maxPlayers); - GadgetListBoxAddEntryText(win, playersStr, gameColor, index, COLUMN_NUMPLAYERS); - - GadgetListBoxAddEntryText(win, L" ", gameColor, index, COLUMN_PASSWORD); // No password for now - } - - // Update game info list box if we had one -} - void ToggleGameListType() { isSmall = !isSmall; diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_json.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_json.h index 1760102f18e..fcaa0d95b32 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_json.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_json.h @@ -6,7 +6,15 @@ #undef min #undef max +#if __has_include() #include +#elif __has_include("GameNetwork/GeneralsOnline/json.hpp") +#include "GameNetwork/GeneralsOnline/json.hpp" +#elif __has_include("json.hpp") +#include "json.hpp" +#else +#include "GameNetwork/GeneralsOnline/json.hpp" +#endif #pragma pop_macro("max") #pragma pop_macro("min") diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/json.hpp b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/json.hpp new file mode 100644 index 00000000000..038cd86171d --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/json.hpp @@ -0,0 +1,25510 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + +/****************************************************************************\ + * Note on documentation: The source files contain links to the online * + * documentation of the public API at https://json.nlohmann.me. This URL * + * contains the most recent documentation and should also be applicable to * + * previous versions; documentation for deprecated functions is not * + * removed, but marked deprecated. See "Generate documentation" section in * + * file docs/README.md. * +\****************************************************************************/ + +#ifndef INCLUDE_NLOHMANN_JSON_HPP_ +#define INCLUDE_NLOHMANN_JSON_HPP_ + +#include // all_of, find, for_each +#include // nullptr_t, ptrdiff_t, size_t +#include // hash, less +#include // initializer_list +#ifndef JSON_NO_IO + #include // istream, ostream +#endif // JSON_NO_IO +#include // random_access_iterator_tag +#include // unique_ptr +#include // string, stoi, to_string +#include // declval, forward, move, pair, swap +#include // vector + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// This file contains all macro definitions affecting or depending on the ABI + +#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK + #if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH) + #if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 11 || NLOHMANN_JSON_VERSION_PATCH != 3 + #warning "Already included a different version of the library!" + #endif + #endif +#endif + +#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum) +#define NLOHMANN_JSON_VERSION_MINOR 11 // NOLINT(modernize-macro-to-enum) +#define NLOHMANN_JSON_VERSION_PATCH 3 // NOLINT(modernize-macro-to-enum) + +#ifndef JSON_DIAGNOSTICS + #define JSON_DIAGNOSTICS 0 +#endif + +#ifndef JSON_DIAGNOSTIC_POSITIONS + #define JSON_DIAGNOSTIC_POSITIONS 0 +#endif + +#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON + #define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0 +#endif + +#if JSON_DIAGNOSTICS + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag +#else + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS +#endif + +#if JSON_DIAGNOSTIC_POSITIONS + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS _dp +#else + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS +#endif + +#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON + #define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp +#else + #define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION + #define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0 +#endif + +// Construct the namespace ABI tags component +#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) json_abi ## a ## b ## c +#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c) \ + NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) + +#define NLOHMANN_JSON_ABI_TAGS \ + NLOHMANN_JSON_ABI_TAGS_CONCAT( \ + NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \ + NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON, \ + NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS) + +// Construct the namespace version component +#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \ + _v ## major ## _ ## minor ## _ ## patch +#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \ + NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) + +#if NLOHMANN_JSON_NAMESPACE_NO_VERSION +#define NLOHMANN_JSON_NAMESPACE_VERSION +#else +#define NLOHMANN_JSON_NAMESPACE_VERSION \ + NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \ + NLOHMANN_JSON_VERSION_MINOR, \ + NLOHMANN_JSON_VERSION_PATCH) +#endif + +// Combine namespace components +#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b +#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \ + NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) + +#ifndef NLOHMANN_JSON_NAMESPACE +#define NLOHMANN_JSON_NAMESPACE \ + nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \ + NLOHMANN_JSON_ABI_TAGS, \ + NLOHMANN_JSON_NAMESPACE_VERSION) +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN +#define NLOHMANN_JSON_NAMESPACE_BEGIN \ + namespace nlohmann \ + { \ + inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \ + NLOHMANN_JSON_ABI_TAGS, \ + NLOHMANN_JSON_NAMESPACE_VERSION) \ + { +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_END +#define NLOHMANN_JSON_NAMESPACE_END \ + } /* namespace (inline namespace) NOLINT(readability/namespace) */ \ + } // namespace nlohmann +#endif + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // transform +#include // array +#include // forward_list +#include // inserter, front_inserter, end +#include // map +#ifdef JSON_HAS_CPP_17 + #include // optional +#endif +#include // string +#include // tuple, make_tuple +#include // is_arithmetic, is_same, is_enum, underlying_type, is_convertible +#include // unordered_map +#include // pair, declval +#include // valarray + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // nullptr_t +#include // exception +#if JSON_DIAGNOSTICS + #include // accumulate +#endif +#include // runtime_error +#include // to_string +#include // vector + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // array +#include // size_t +#include // uint8_t +#include // string + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // declval, pair +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +template struct make_void +{ + using type = void; +}; +template using void_t = typename make_void::type; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +// https://en.cppreference.com/w/cpp/experimental/is_detected +struct nonesuch +{ + nonesuch() = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const&) = delete; + nonesuch(nonesuch const&&) = delete; + void operator=(nonesuch const&) = delete; + void operator=(nonesuch&&) = delete; +}; + +template class Op, + class... Args> +struct detector +{ + using value_t = std::false_type; + using type = Default; +}; + +template class Op, class... Args> +struct detector>, Op, Args...> +{ + using value_t = std::true_type; + using type = Op; +}; + +template class Op, class... Args> +using is_detected = typename detector::value_t; + +template class Op, class... Args> +struct is_detected_lazy : is_detected { }; + +template class Op, class... Args> +using detected_t = typename detector::type; + +template class Op, class... Args> +using detected_or = detector; + +template class Op, class... Args> +using detected_or_t = typename detected_or::type; + +template class Op, class... Args> +using is_detected_exact = std::is_same>; + +template class Op, class... Args> +using is_detected_convertible = + std::is_convertible, To>; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + + +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-FileCopyrightText: 2016 - 2021 Evan Nemerson +// SPDX-License-Identifier: MIT + +/* Hedley - https://nemequ.github.io/hedley + * Created by Evan Nemerson + */ + +#if !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < 15) +#if defined(JSON_HEDLEY_VERSION) + #undef JSON_HEDLEY_VERSION +#endif +#define JSON_HEDLEY_VERSION 15 + +#if defined(JSON_HEDLEY_STRINGIFY_EX) + #undef JSON_HEDLEY_STRINGIFY_EX +#endif +#define JSON_HEDLEY_STRINGIFY_EX(x) #x + +#if defined(JSON_HEDLEY_STRINGIFY) + #undef JSON_HEDLEY_STRINGIFY +#endif +#define JSON_HEDLEY_STRINGIFY(x) JSON_HEDLEY_STRINGIFY_EX(x) + +#if defined(JSON_HEDLEY_CONCAT_EX) + #undef JSON_HEDLEY_CONCAT_EX +#endif +#define JSON_HEDLEY_CONCAT_EX(a,b) a##b + +#if defined(JSON_HEDLEY_CONCAT) + #undef JSON_HEDLEY_CONCAT +#endif +#define JSON_HEDLEY_CONCAT(a,b) JSON_HEDLEY_CONCAT_EX(a,b) + +#if defined(JSON_HEDLEY_CONCAT3_EX) + #undef JSON_HEDLEY_CONCAT3_EX +#endif +#define JSON_HEDLEY_CONCAT3_EX(a,b,c) a##b##c + +#if defined(JSON_HEDLEY_CONCAT3) + #undef JSON_HEDLEY_CONCAT3 +#endif +#define JSON_HEDLEY_CONCAT3(a,b,c) JSON_HEDLEY_CONCAT3_EX(a,b,c) + +#if defined(JSON_HEDLEY_VERSION_ENCODE) + #undef JSON_HEDLEY_VERSION_ENCODE +#endif +#define JSON_HEDLEY_VERSION_ENCODE(major,minor,revision) (((major) * 1000000) + ((minor) * 1000) + (revision)) + +#if defined(JSON_HEDLEY_VERSION_DECODE_MAJOR) + #undef JSON_HEDLEY_VERSION_DECODE_MAJOR +#endif +#define JSON_HEDLEY_VERSION_DECODE_MAJOR(version) ((version) / 1000000) + +#if defined(JSON_HEDLEY_VERSION_DECODE_MINOR) + #undef JSON_HEDLEY_VERSION_DECODE_MINOR +#endif +#define JSON_HEDLEY_VERSION_DECODE_MINOR(version) (((version) % 1000000) / 1000) + +#if defined(JSON_HEDLEY_VERSION_DECODE_REVISION) + #undef JSON_HEDLEY_VERSION_DECODE_REVISION +#endif +#define JSON_HEDLEY_VERSION_DECODE_REVISION(version) ((version) % 1000) + +#if defined(JSON_HEDLEY_GNUC_VERSION) + #undef JSON_HEDLEY_GNUC_VERSION +#endif +#if defined(__GNUC__) && defined(__GNUC_PATCHLEVEL__) + #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#elif defined(__GNUC__) + #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, 0) +#endif + +#if defined(JSON_HEDLEY_GNUC_VERSION_CHECK) + #undef JSON_HEDLEY_GNUC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_GNUC_VERSION) + #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GNUC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_MSVC_VERSION) + #undef JSON_HEDLEY_MSVC_VERSION +#endif +#if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 140000000) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 10000000, (_MSC_FULL_VER % 10000000) / 100000, (_MSC_FULL_VER % 100000) / 100) +#elif defined(_MSC_FULL_VER) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 1000000, (_MSC_FULL_VER % 1000000) / 10000, (_MSC_FULL_VER % 10000) / 10) +#elif defined(_MSC_VER) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_VER / 100, _MSC_VER % 100, 0) +#endif + +#if defined(JSON_HEDLEY_MSVC_VERSION_CHECK) + #undef JSON_HEDLEY_MSVC_VERSION_CHECK +#endif +#if !defined(JSON_HEDLEY_MSVC_VERSION) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (0) +#elif defined(_MSC_VER) && (_MSC_VER >= 1400) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 10000000) + (minor * 100000) + (patch))) +#elif defined(_MSC_VER) && (_MSC_VER >= 1200) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 1000000) + (minor * 10000) + (patch))) +#else + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_VER >= ((major * 100) + (minor))) +#endif + +#if defined(JSON_HEDLEY_INTEL_VERSION) + #undef JSON_HEDLEY_INTEL_VERSION +#endif +#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) && !defined(__ICL) + #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, __INTEL_COMPILER_UPDATE) +#elif defined(__INTEL_COMPILER) && !defined(__ICL) + #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, 0) +#endif + +#if defined(JSON_HEDLEY_INTEL_VERSION_CHECK) + #undef JSON_HEDLEY_INTEL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_INTEL_VERSION) + #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_INTEL_CL_VERSION) + #undef JSON_HEDLEY_INTEL_CL_VERSION +#endif +#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) && defined(__ICL) + #define JSON_HEDLEY_INTEL_CL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER, __INTEL_COMPILER_UPDATE, 0) +#endif + +#if defined(JSON_HEDLEY_INTEL_CL_VERSION_CHECK) + #undef JSON_HEDLEY_INTEL_CL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_INTEL_CL_VERSION) + #define JSON_HEDLEY_INTEL_CL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_CL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_INTEL_CL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_PGI_VERSION) + #undef JSON_HEDLEY_PGI_VERSION +#endif +#if defined(__PGI) && defined(__PGIC__) && defined(__PGIC_MINOR__) && defined(__PGIC_PATCHLEVEL__) + #define JSON_HEDLEY_PGI_VERSION JSON_HEDLEY_VERSION_ENCODE(__PGIC__, __PGIC_MINOR__, __PGIC_PATCHLEVEL__) +#endif + +#if defined(JSON_HEDLEY_PGI_VERSION_CHECK) + #undef JSON_HEDLEY_PGI_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_PGI_VERSION) + #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PGI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_SUNPRO_VERSION) + #undef JSON_HEDLEY_SUNPRO_VERSION +#endif +#if defined(__SUNPRO_C) && (__SUNPRO_C > 0x1000) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_C >> 16) & 0xf) * 10) + ((__SUNPRO_C >> 12) & 0xf), (((__SUNPRO_C >> 8) & 0xf) * 10) + ((__SUNPRO_C >> 4) & 0xf), (__SUNPRO_C & 0xf) * 10) +#elif defined(__SUNPRO_C) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_C >> 8) & 0xf, (__SUNPRO_C >> 4) & 0xf, (__SUNPRO_C) & 0xf) +#elif defined(__SUNPRO_CC) && (__SUNPRO_CC > 0x1000) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_CC >> 16) & 0xf) * 10) + ((__SUNPRO_CC >> 12) & 0xf), (((__SUNPRO_CC >> 8) & 0xf) * 10) + ((__SUNPRO_CC >> 4) & 0xf), (__SUNPRO_CC & 0xf) * 10) +#elif defined(__SUNPRO_CC) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_CC >> 8) & 0xf, (__SUNPRO_CC >> 4) & 0xf, (__SUNPRO_CC) & 0xf) +#endif + +#if defined(JSON_HEDLEY_SUNPRO_VERSION_CHECK) + #undef JSON_HEDLEY_SUNPRO_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_SUNPRO_VERSION) + #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_SUNPRO_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) + #undef JSON_HEDLEY_EMSCRIPTEN_VERSION +#endif +#if defined(__EMSCRIPTEN__) + #define JSON_HEDLEY_EMSCRIPTEN_VERSION JSON_HEDLEY_VERSION_ENCODE(__EMSCRIPTEN_major__, __EMSCRIPTEN_minor__, __EMSCRIPTEN_tiny__) +#endif + +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK) + #undef JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) + #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_EMSCRIPTEN_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_ARM_VERSION) + #undef JSON_HEDLEY_ARM_VERSION +#endif +#if defined(__CC_ARM) && defined(__ARMCOMPILER_VERSION) + #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCOMPILER_VERSION / 1000000, (__ARMCOMPILER_VERSION % 1000000) / 10000, (__ARMCOMPILER_VERSION % 10000) / 100) +#elif defined(__CC_ARM) && defined(__ARMCC_VERSION) + #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCC_VERSION / 1000000, (__ARMCC_VERSION % 1000000) / 10000, (__ARMCC_VERSION % 10000) / 100) +#endif + +#if defined(JSON_HEDLEY_ARM_VERSION_CHECK) + #undef JSON_HEDLEY_ARM_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_ARM_VERSION) + #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_ARM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_IBM_VERSION) + #undef JSON_HEDLEY_IBM_VERSION +#endif +#if defined(__ibmxl__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ibmxl_version__, __ibmxl_release__, __ibmxl_modification__) +#elif defined(__xlC__) && defined(__xlC_ver__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, (__xlC_ver__ >> 8) & 0xff) +#elif defined(__xlC__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, 0) +#endif + +#if defined(JSON_HEDLEY_IBM_VERSION_CHECK) + #undef JSON_HEDLEY_IBM_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_IBM_VERSION) + #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IBM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_VERSION) + #undef JSON_HEDLEY_TI_VERSION +#endif +#if \ + defined(__TI_COMPILER_VERSION__) && \ + ( \ + defined(__TMS470__) || defined(__TI_ARM__) || \ + defined(__MSP430__) || \ + defined(__TMS320C2000__) \ + ) +#if (__TI_COMPILER_VERSION__ >= 16000000) + #define JSON_HEDLEY_TI_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif +#endif + +#if defined(JSON_HEDLEY_TI_VERSION_CHECK) + #undef JSON_HEDLEY_TI_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_VERSION) + #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL2000_VERSION) + #undef JSON_HEDLEY_TI_CL2000_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C2000__) + #define JSON_HEDLEY_TI_CL2000_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL2000_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL2000_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL2000_VERSION) + #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL2000_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL430_VERSION) + #undef JSON_HEDLEY_TI_CL430_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__MSP430__) + #define JSON_HEDLEY_TI_CL430_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL430_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL430_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL430_VERSION) + #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL430_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) + #undef JSON_HEDLEY_TI_ARMCL_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && (defined(__TMS470__) || defined(__TI_ARM__)) + #define JSON_HEDLEY_TI_ARMCL_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION_CHECK) + #undef JSON_HEDLEY_TI_ARMCL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) + #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_ARMCL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL6X_VERSION) + #undef JSON_HEDLEY_TI_CL6X_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C6X__) + #define JSON_HEDLEY_TI_CL6X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL6X_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL6X_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL6X_VERSION) + #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL6X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL7X_VERSION) + #undef JSON_HEDLEY_TI_CL7X_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__C7000__) + #define JSON_HEDLEY_TI_CL7X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL7X_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL7X_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL7X_VERSION) + #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL7X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) + #undef JSON_HEDLEY_TI_CLPRU_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__PRU__) + #define JSON_HEDLEY_TI_CLPRU_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CLPRU_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) + #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CLPRU_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_CRAY_VERSION) + #undef JSON_HEDLEY_CRAY_VERSION +#endif +#if defined(_CRAYC) + #if defined(_RELEASE_PATCHLEVEL) + #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, _RELEASE_PATCHLEVEL) + #else + #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, 0) + #endif +#endif + +#if defined(JSON_HEDLEY_CRAY_VERSION_CHECK) + #undef JSON_HEDLEY_CRAY_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_CRAY_VERSION) + #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_CRAY_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_IAR_VERSION) + #undef JSON_HEDLEY_IAR_VERSION +#endif +#if defined(__IAR_SYSTEMS_ICC__) + #if __VER__ > 1000 + #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE((__VER__ / 1000000), ((__VER__ / 1000) % 1000), (__VER__ % 1000)) + #else + #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE(__VER__ / 100, __VER__ % 100, 0) + #endif +#endif + +#if defined(JSON_HEDLEY_IAR_VERSION_CHECK) + #undef JSON_HEDLEY_IAR_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_IAR_VERSION) + #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IAR_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TINYC_VERSION) + #undef JSON_HEDLEY_TINYC_VERSION +#endif +#if defined(__TINYC__) + #define JSON_HEDLEY_TINYC_VERSION JSON_HEDLEY_VERSION_ENCODE(__TINYC__ / 1000, (__TINYC__ / 100) % 10, __TINYC__ % 100) +#endif + +#if defined(JSON_HEDLEY_TINYC_VERSION_CHECK) + #undef JSON_HEDLEY_TINYC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TINYC_VERSION) + #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TINYC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_DMC_VERSION) + #undef JSON_HEDLEY_DMC_VERSION +#endif +#if defined(__DMC__) + #define JSON_HEDLEY_DMC_VERSION JSON_HEDLEY_VERSION_ENCODE(__DMC__ >> 8, (__DMC__ >> 4) & 0xf, __DMC__ & 0xf) +#endif + +#if defined(JSON_HEDLEY_DMC_VERSION_CHECK) + #undef JSON_HEDLEY_DMC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_DMC_VERSION) + #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_DMC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_COMPCERT_VERSION) + #undef JSON_HEDLEY_COMPCERT_VERSION +#endif +#if defined(__COMPCERT_VERSION__) + #define JSON_HEDLEY_COMPCERT_VERSION JSON_HEDLEY_VERSION_ENCODE(__COMPCERT_VERSION__ / 10000, (__COMPCERT_VERSION__ / 100) % 100, __COMPCERT_VERSION__ % 100) +#endif + +#if defined(JSON_HEDLEY_COMPCERT_VERSION_CHECK) + #undef JSON_HEDLEY_COMPCERT_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_COMPCERT_VERSION) + #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_COMPCERT_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_PELLES_VERSION) + #undef JSON_HEDLEY_PELLES_VERSION +#endif +#if defined(__POCC__) + #define JSON_HEDLEY_PELLES_VERSION JSON_HEDLEY_VERSION_ENCODE(__POCC__ / 100, __POCC__ % 100, 0) +#endif + +#if defined(JSON_HEDLEY_PELLES_VERSION_CHECK) + #undef JSON_HEDLEY_PELLES_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_PELLES_VERSION) + #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PELLES_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_MCST_LCC_VERSION) + #undef JSON_HEDLEY_MCST_LCC_VERSION +#endif +#if defined(__LCC__) && defined(__LCC_MINOR__) + #define JSON_HEDLEY_MCST_LCC_VERSION JSON_HEDLEY_VERSION_ENCODE(__LCC__ / 100, __LCC__ % 100, __LCC_MINOR__) +#endif + +#if defined(JSON_HEDLEY_MCST_LCC_VERSION_CHECK) + #undef JSON_HEDLEY_MCST_LCC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_MCST_LCC_VERSION) + #define JSON_HEDLEY_MCST_LCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_MCST_LCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_MCST_LCC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_GCC_VERSION) + #undef JSON_HEDLEY_GCC_VERSION +#endif +#if \ + defined(JSON_HEDLEY_GNUC_VERSION) && \ + !defined(__clang__) && \ + !defined(JSON_HEDLEY_INTEL_VERSION) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_ARM_VERSION) && \ + !defined(JSON_HEDLEY_CRAY_VERSION) && \ + !defined(JSON_HEDLEY_TI_VERSION) && \ + !defined(JSON_HEDLEY_TI_ARMCL_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL430_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL2000_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL6X_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL7X_VERSION) && \ + !defined(JSON_HEDLEY_TI_CLPRU_VERSION) && \ + !defined(__COMPCERT__) && \ + !defined(JSON_HEDLEY_MCST_LCC_VERSION) + #define JSON_HEDLEY_GCC_VERSION JSON_HEDLEY_GNUC_VERSION +#endif + +#if defined(JSON_HEDLEY_GCC_VERSION_CHECK) + #undef JSON_HEDLEY_GCC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_GCC_VERSION) + #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_ATTRIBUTE +#endif +#if \ + defined(__has_attribute) && \ + ( \ + (!defined(JSON_HEDLEY_IAR_VERSION) || JSON_HEDLEY_IAR_VERSION_CHECK(8,5,9)) \ + ) +# define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) __has_attribute(attribute) +#else +# define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE +#endif +#if \ + defined(__has_cpp_attribute) && \ + defined(__cplusplus) && \ + (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS) + #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS +#endif +#if !defined(__cplusplus) || !defined(__has_cpp_attribute) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) +#elif \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_IAR_VERSION) && \ + (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) && \ + (!defined(JSON_HEDLEY_MSVC_VERSION) || JSON_HEDLEY_MSVC_VERSION_CHECK(19,20,0)) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(ns::attribute) +#else + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE +#endif +#if defined(__has_cpp_attribute) && defined(__cplusplus) + #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE +#endif +#if defined(__has_cpp_attribute) && defined(__cplusplus) + #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_BUILTIN) + #undef JSON_HEDLEY_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_HAS_BUILTIN(builtin) __has_builtin(builtin) +#else + #define JSON_HEDLEY_HAS_BUILTIN(builtin) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_BUILTIN) + #undef JSON_HEDLEY_GNUC_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin) +#else + #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_BUILTIN) + #undef JSON_HEDLEY_GCC_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin) +#else + #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_FEATURE) + #undef JSON_HEDLEY_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_HAS_FEATURE(feature) __has_feature(feature) +#else + #define JSON_HEDLEY_HAS_FEATURE(feature) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_FEATURE) + #undef JSON_HEDLEY_GNUC_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature) +#else + #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_FEATURE) + #undef JSON_HEDLEY_GCC_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature) +#else + #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_EXTENSION) + #undef JSON_HEDLEY_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_HAS_EXTENSION(extension) __has_extension(extension) +#else + #define JSON_HEDLEY_HAS_EXTENSION(extension) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_EXTENSION) + #undef JSON_HEDLEY_GNUC_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension) +#else + #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_EXTENSION) + #undef JSON_HEDLEY_GCC_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension) +#else + #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_WARNING) + #undef JSON_HEDLEY_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_HAS_WARNING(warning) __has_warning(warning) +#else + #define JSON_HEDLEY_HAS_WARNING(warning) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_WARNING) + #undef JSON_HEDLEY_GNUC_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning) +#else + #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_WARNING) + #undef JSON_HEDLEY_GCC_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning) +#else + #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ + defined(__clang__) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,17) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(8,0,0) || \ + (JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) && defined(__C99_PRAGMA_OPERATOR)) + #define JSON_HEDLEY_PRAGMA(value) _Pragma(#value) +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_PRAGMA(value) __pragma(value) +#else + #define JSON_HEDLEY_PRAGMA(value) +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_PUSH) + #undef JSON_HEDLEY_DIAGNOSTIC_PUSH +#endif +#if defined(JSON_HEDLEY_DIAGNOSTIC_POP) + #undef JSON_HEDLEY_DIAGNOSTIC_POP +#endif +#if defined(__clang__) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("clang diagnostic push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("clang diagnostic pop") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH __pragma(warning(push)) + #define JSON_HEDLEY_DIAGNOSTIC_POP __pragma(warning(pop)) +#elif JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("pop") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,4,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("diag_push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("diag_pop") +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)") +#else + #define JSON_HEDLEY_DIAGNOSTIC_PUSH + #define JSON_HEDLEY_DIAGNOSTIC_POP +#endif + +/* JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ is for + HEDLEY INTERNAL USE ONLY. API subject to change without notice. */ +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ +#endif +#if defined(__cplusplus) +# if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat") +# if JSON_HEDLEY_HAS_WARNING("-Wc++17-extensions") +# if JSON_HEDLEY_HAS_WARNING("-Wc++1z-extensions") +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + _Pragma("clang diagnostic ignored \"-Wc++17-extensions\"") \ + _Pragma("clang diagnostic ignored \"-Wc++1z-extensions\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + _Pragma("clang diagnostic ignored \"-Wc++17-extensions\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# endif +# else +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# endif +# endif +#endif +#if !defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(x) x +#endif + +#if defined(JSON_HEDLEY_CONST_CAST) + #undef JSON_HEDLEY_CONST_CAST +#endif +#if defined(__cplusplus) +# define JSON_HEDLEY_CONST_CAST(T, expr) (const_cast(expr)) +#elif \ + JSON_HEDLEY_HAS_WARNING("-Wcast-qual") || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_CONST_CAST(T, expr) (__extension__ ({ \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL \ + ((T) (expr)); \ + JSON_HEDLEY_DIAGNOSTIC_POP \ + })) +#else +# define JSON_HEDLEY_CONST_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_REINTERPRET_CAST) + #undef JSON_HEDLEY_REINTERPRET_CAST +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) (reinterpret_cast(expr)) +#else + #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_STATIC_CAST) + #undef JSON_HEDLEY_STATIC_CAST +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_STATIC_CAST(T, expr) (static_cast(expr)) +#else + #define JSON_HEDLEY_STATIC_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_CPP_CAST) + #undef JSON_HEDLEY_CPP_CAST +#endif +#if defined(__cplusplus) +# if JSON_HEDLEY_HAS_WARNING("-Wold-style-cast") +# define JSON_HEDLEY_CPP_CAST(T, expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wold-style-cast\"") \ + ((T) (expr)) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# elif JSON_HEDLEY_IAR_VERSION_CHECK(8,3,0) +# define JSON_HEDLEY_CPP_CAST(T, expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("diag_suppress=Pe137") \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_CPP_CAST(T, expr) ((T) (expr)) +# endif +#else +# define JSON_HEDLEY_CPP_CAST(T, expr) (expr) +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wdeprecated-declarations") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warning(disable:1478 1786)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED __pragma(warning(disable:1478 1786)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(20,7,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1216,1444,1445") +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1444") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED __pragma(warning(disable:4996)) +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1444") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1291,1718") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && !defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,E_DEPRECATED_ATT,E_DEPRECATED_ATT_MESS)") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,symdeprecated,symdeprecated2)") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress=Pe1444,Pe1215") +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warn(disable:2241)") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("clang diagnostic ignored \"-Wunknown-pragmas\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("warning(disable:161)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS __pragma(warning(disable:161)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 1675") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("GCC diagnostic ignored \"-Wunknown-pragmas\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS __pragma(warning(disable:4068)) +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(16,9,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163") +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress=Pe161") +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 161") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-attributes") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("clang diagnostic ignored \"-Wunknown-attributes\"") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("warning(disable:1292)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES __pragma(warning(disable:1292)) +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES __pragma(warning(disable:5030)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(20,7,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097,1098") +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("error_messages(off,attrskipunsup)") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1173") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress=Pe1097") +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wcast-qual") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("clang diagnostic ignored \"-Wcast-qual\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("warning(disable:2203 2331)") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("GCC diagnostic ignored \"-Wcast-qual\"") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunused-function") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("clang diagnostic ignored \"-Wunused-function\"") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("GCC diagnostic ignored \"-Wunused-function\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(1,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION __pragma(warning(disable:4505)) +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("diag_suppress 3142") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION +#endif + +#if defined(JSON_HEDLEY_DEPRECATED) + #undef JSON_HEDLEY_DEPRECATED +#endif +#if defined(JSON_HEDLEY_DEPRECATED_FOR) + #undef JSON_HEDLEY_DEPRECATED_FOR +#endif +#if \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated("Since " # since)) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated("Since " #since "; use " #replacement)) +#elif \ + (JSON_HEDLEY_HAS_EXTENSION(attribute_deprecated_with_message) && !defined(JSON_HEDLEY_IAR_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__("Since " #since))) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__("Since " #since "; use " #replacement))) +#elif defined(__cplusplus) && (__cplusplus >= 201402L) + #define JSON_HEDLEY_DEPRECATED(since) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since)]]) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since "; use " #replacement)]]) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(deprecated) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__)) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_PELLES_VERSION_CHECK(6,50,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated) +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DEPRECATED(since) _Pragma("deprecated") + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) _Pragma("deprecated") +#else + #define JSON_HEDLEY_DEPRECATED(since) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) +#endif + +#if defined(JSON_HEDLEY_UNAVAILABLE) + #undef JSON_HEDLEY_UNAVAILABLE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(warning) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_UNAVAILABLE(available_since) __attribute__((__warning__("Not available until " #available_since))) +#else + #define JSON_HEDLEY_UNAVAILABLE(available_since) +#endif + +#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT) + #undef JSON_HEDLEY_WARN_UNUSED_RESULT +#endif +#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT_MSG) + #undef JSON_HEDLEY_WARN_UNUSED_RESULT_MSG +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(warn_unused_result) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_WARN_UNUSED_RESULT __attribute__((__warn_unused_result__)) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) __attribute__((__warn_unused_result__)) +#elif (JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) >= 201907L) + #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard(msg)]]) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) + #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) +#elif defined(_Check_return_) /* SAL */ + #define JSON_HEDLEY_WARN_UNUSED_RESULT _Check_return_ + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) _Check_return_ +#else + #define JSON_HEDLEY_WARN_UNUSED_RESULT + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) +#endif + +#if defined(JSON_HEDLEY_SENTINEL) + #undef JSON_HEDLEY_SENTINEL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(sentinel) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_SENTINEL(position) __attribute__((__sentinel__(position))) +#else + #define JSON_HEDLEY_SENTINEL(position) +#endif + +#if defined(JSON_HEDLEY_NO_RETURN) + #undef JSON_HEDLEY_NO_RETURN +#endif +#if JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_NO_RETURN __noreturn +#elif \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__)) +#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L + #define JSON_HEDLEY_NO_RETURN _Noreturn +#elif defined(__cplusplus) && (__cplusplus >= 201103L) + #define JSON_HEDLEY_NO_RETURN JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[noreturn]]) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(noreturn) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,2,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_NO_RETURN _Pragma("does_not_return") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_NO_RETURN __declspec(noreturn) +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus) + #define JSON_HEDLEY_NO_RETURN _Pragma("FUNC_NEVER_RETURNS;") +#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0) + #define JSON_HEDLEY_NO_RETURN __attribute((noreturn)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0) + #define JSON_HEDLEY_NO_RETURN __declspec(noreturn) +#else + #define JSON_HEDLEY_NO_RETURN +#endif + +#if defined(JSON_HEDLEY_NO_ESCAPE) + #undef JSON_HEDLEY_NO_ESCAPE +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(noescape) + #define JSON_HEDLEY_NO_ESCAPE __attribute__((__noescape__)) +#else + #define JSON_HEDLEY_NO_ESCAPE +#endif + +#if defined(JSON_HEDLEY_UNREACHABLE) + #undef JSON_HEDLEY_UNREACHABLE +#endif +#if defined(JSON_HEDLEY_UNREACHABLE_RETURN) + #undef JSON_HEDLEY_UNREACHABLE_RETURN +#endif +#if defined(JSON_HEDLEY_ASSUME) + #undef JSON_HEDLEY_ASSUME +#endif +#if \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_ASSUME(expr) __assume(expr) +#elif JSON_HEDLEY_HAS_BUILTIN(__builtin_assume) + #define JSON_HEDLEY_ASSUME(expr) __builtin_assume(expr) +#elif \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) + #if defined(__cplusplus) + #define JSON_HEDLEY_ASSUME(expr) std::_nassert(expr) + #else + #define JSON_HEDLEY_ASSUME(expr) _nassert(expr) + #endif +#endif +#if \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_unreachable) && (!defined(JSON_HEDLEY_ARM_VERSION))) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,10,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,5) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(10,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_UNREACHABLE() __builtin_unreachable() +#elif defined(JSON_HEDLEY_ASSUME) + #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0) +#endif +#if !defined(JSON_HEDLEY_ASSUME) + #if defined(JSON_HEDLEY_UNREACHABLE) + #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, ((expr) ? 1 : (JSON_HEDLEY_UNREACHABLE(), 1))) + #else + #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, expr) + #endif +#endif +#if defined(JSON_HEDLEY_UNREACHABLE) + #if \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (JSON_HEDLEY_STATIC_CAST(void, JSON_HEDLEY_ASSUME(0)), (value)) + #else + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) JSON_HEDLEY_UNREACHABLE() + #endif +#else + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (value) +#endif +#if !defined(JSON_HEDLEY_UNREACHABLE) + #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0) +#endif + +JSON_HEDLEY_DIAGNOSTIC_PUSH +#if JSON_HEDLEY_HAS_WARNING("-Wpedantic") + #pragma clang diagnostic ignored "-Wpedantic" +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat-pedantic") && defined(__cplusplus) + #pragma clang diagnostic ignored "-Wc++98-compat-pedantic" +#endif +#if JSON_HEDLEY_GCC_HAS_WARNING("-Wvariadic-macros",4,0,0) + #if defined(__clang__) + #pragma clang diagnostic ignored "-Wvariadic-macros" + #elif defined(JSON_HEDLEY_GCC_VERSION) + #pragma GCC diagnostic ignored "-Wvariadic-macros" + #endif +#endif +#if defined(JSON_HEDLEY_NON_NULL) + #undef JSON_HEDLEY_NON_NULL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(nonnull) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) + #define JSON_HEDLEY_NON_NULL(...) __attribute__((__nonnull__(__VA_ARGS__))) +#else + #define JSON_HEDLEY_NON_NULL(...) +#endif +JSON_HEDLEY_DIAGNOSTIC_POP + +#if defined(JSON_HEDLEY_PRINTF_FORMAT) + #undef JSON_HEDLEY_PRINTF_FORMAT +#endif +#if defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && !defined(__USE_MINGW_ANSI_STDIO) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(ms_printf, string_idx, first_to_check))) +#elif defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && defined(__USE_MINGW_ANSI_STDIO) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(gnu_printf, string_idx, first_to_check))) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(format) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(__printf__, string_idx, first_to_check))) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(6,0,0) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __declspec(vaformat(printf,string_idx,first_to_check)) +#else + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) +#endif + +#if defined(JSON_HEDLEY_CONSTEXPR) + #undef JSON_HEDLEY_CONSTEXPR +#endif +#if defined(__cplusplus) + #if __cplusplus >= 201103L + #define JSON_HEDLEY_CONSTEXPR JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(constexpr) + #endif +#endif +#if !defined(JSON_HEDLEY_CONSTEXPR) + #define JSON_HEDLEY_CONSTEXPR +#endif + +#if defined(JSON_HEDLEY_PREDICT) + #undef JSON_HEDLEY_PREDICT +#endif +#if defined(JSON_HEDLEY_LIKELY) + #undef JSON_HEDLEY_LIKELY +#endif +#if defined(JSON_HEDLEY_UNLIKELY) + #undef JSON_HEDLEY_UNLIKELY +#endif +#if defined(JSON_HEDLEY_UNPREDICTABLE) + #undef JSON_HEDLEY_UNPREDICTABLE +#endif +#if JSON_HEDLEY_HAS_BUILTIN(__builtin_unpredictable) + #define JSON_HEDLEY_UNPREDICTABLE(expr) __builtin_unpredictable((expr)) +#endif +#if \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_expect_with_probability) && !defined(JSON_HEDLEY_PGI_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(9,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PREDICT(expr, value, probability) __builtin_expect_with_probability( (expr), (value), (probability)) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) __builtin_expect_with_probability(!!(expr), 1 , (probability)) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) __builtin_expect_with_probability(!!(expr), 0 , (probability)) +# define JSON_HEDLEY_LIKELY(expr) __builtin_expect (!!(expr), 1 ) +# define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect (!!(expr), 0 ) +#elif \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_expect) && !defined(JSON_HEDLEY_INTEL_CL_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,27) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PREDICT(expr, expected, probability) \ + (((probability) >= 0.9) ? __builtin_expect((expr), (expected)) : (JSON_HEDLEY_STATIC_CAST(void, expected), (expr))) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) \ + (__extension__ ({ \ + double hedley_probability_ = (probability); \ + ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 1) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 0) : !!(expr))); \ + })) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) \ + (__extension__ ({ \ + double hedley_probability_ = (probability); \ + ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 0) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 1) : !!(expr))); \ + })) +# define JSON_HEDLEY_LIKELY(expr) __builtin_expect(!!(expr), 1) +# define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect(!!(expr), 0) +#else +# define JSON_HEDLEY_PREDICT(expr, expected, probability) (JSON_HEDLEY_STATIC_CAST(void, expected), (expr)) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) (!!(expr)) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) (!!(expr)) +# define JSON_HEDLEY_LIKELY(expr) (!!(expr)) +# define JSON_HEDLEY_UNLIKELY(expr) (!!(expr)) +#endif +#if !defined(JSON_HEDLEY_UNPREDICTABLE) + #define JSON_HEDLEY_UNPREDICTABLE(expr) JSON_HEDLEY_PREDICT(expr, 1, 0.5) +#endif + +#if defined(JSON_HEDLEY_MALLOC) + #undef JSON_HEDLEY_MALLOC +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(malloc) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_MALLOC __attribute__((__malloc__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_MALLOC _Pragma("returns_new_memory") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_MALLOC __declspec(restrict) +#else + #define JSON_HEDLEY_MALLOC +#endif + +#if defined(JSON_HEDLEY_PURE) + #undef JSON_HEDLEY_PURE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(pure) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(2,96,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PURE __attribute__((__pure__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) +# define JSON_HEDLEY_PURE _Pragma("does_not_write_global_data") +#elif defined(__cplusplus) && \ + ( \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) \ + ) +# define JSON_HEDLEY_PURE _Pragma("FUNC_IS_PURE;") +#else +# define JSON_HEDLEY_PURE +#endif + +#if defined(JSON_HEDLEY_CONST) + #undef JSON_HEDLEY_CONST +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(const) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(2,5,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_CONST __attribute__((__const__)) +#elif \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_CONST _Pragma("no_side_effect") +#else + #define JSON_HEDLEY_CONST JSON_HEDLEY_PURE +#endif + +#if defined(JSON_HEDLEY_RESTRICT) + #undef JSON_HEDLEY_RESTRICT +#endif +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && !defined(__cplusplus) + #define JSON_HEDLEY_RESTRICT restrict +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,4) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \ + defined(__clang__) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_RESTRICT __restrict +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,3,0) && !defined(__cplusplus) + #define JSON_HEDLEY_RESTRICT _Restrict +#else + #define JSON_HEDLEY_RESTRICT +#endif + +#if defined(JSON_HEDLEY_INLINE) + #undef JSON_HEDLEY_INLINE +#endif +#if \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ + (defined(__cplusplus) && (__cplusplus >= 199711L)) + #define JSON_HEDLEY_INLINE inline +#elif \ + defined(JSON_HEDLEY_GCC_VERSION) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(6,2,0) + #define JSON_HEDLEY_INLINE __inline__ +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,1,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_INLINE __inline +#else + #define JSON_HEDLEY_INLINE +#endif + +#if defined(JSON_HEDLEY_ALWAYS_INLINE) + #undef JSON_HEDLEY_ALWAYS_INLINE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(always_inline) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) +# define JSON_HEDLEY_ALWAYS_INLINE __attribute__((__always_inline__)) JSON_HEDLEY_INLINE +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_ALWAYS_INLINE __forceinline +#elif defined(__cplusplus) && \ + ( \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) \ + ) +# define JSON_HEDLEY_ALWAYS_INLINE _Pragma("FUNC_ALWAYS_INLINE;") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) +# define JSON_HEDLEY_ALWAYS_INLINE _Pragma("inline=forced") +#else +# define JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_INLINE +#endif + +#if defined(JSON_HEDLEY_NEVER_INLINE) + #undef JSON_HEDLEY_NEVER_INLINE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(noinline) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_NEVER_INLINE __attribute__((__noinline__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(10,2,0) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("noinline") +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("FUNC_CANNOT_INLINE;") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("inline=never") +#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0) + #define JSON_HEDLEY_NEVER_INLINE __attribute((noinline)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0) + #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline) +#else + #define JSON_HEDLEY_NEVER_INLINE +#endif + +#if defined(JSON_HEDLEY_PRIVATE) + #undef JSON_HEDLEY_PRIVATE +#endif +#if defined(JSON_HEDLEY_PUBLIC) + #undef JSON_HEDLEY_PUBLIC +#endif +#if defined(JSON_HEDLEY_IMPORT) + #undef JSON_HEDLEY_IMPORT +#endif +#if defined(_WIN32) || defined(__CYGWIN__) +# define JSON_HEDLEY_PRIVATE +# define JSON_HEDLEY_PUBLIC __declspec(dllexport) +# define JSON_HEDLEY_IMPORT __declspec(dllimport) +#else +# if \ + JSON_HEDLEY_HAS_ATTRIBUTE(visibility) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + ( \ + defined(__TI_EABI__) && \ + ( \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) \ + ) \ + ) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PRIVATE __attribute__((__visibility__("hidden"))) +# define JSON_HEDLEY_PUBLIC __attribute__((__visibility__("default"))) +# else +# define JSON_HEDLEY_PRIVATE +# define JSON_HEDLEY_PUBLIC +# endif +# define JSON_HEDLEY_IMPORT extern +#endif + +#if defined(JSON_HEDLEY_NO_THROW) + #undef JSON_HEDLEY_NO_THROW +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(nothrow) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_NO_THROW __attribute__((__nothrow__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) + #define JSON_HEDLEY_NO_THROW __declspec(nothrow) +#else + #define JSON_HEDLEY_NO_THROW +#endif + +#if defined(JSON_HEDLEY_FALL_THROUGH) + #undef JSON_HEDLEY_FALL_THROUGH +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(fallthrough) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(7,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_FALL_THROUGH __attribute__((__fallthrough__)) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(clang,fallthrough) + #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[clang::fallthrough]]) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(fallthrough) + #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[fallthrough]]) +#elif defined(__fallthrough) /* SAL */ + #define JSON_HEDLEY_FALL_THROUGH __fallthrough +#else + #define JSON_HEDLEY_FALL_THROUGH +#endif + +#if defined(JSON_HEDLEY_RETURNS_NON_NULL) + #undef JSON_HEDLEY_RETURNS_NON_NULL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(returns_nonnull) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_RETURNS_NON_NULL __attribute__((__returns_nonnull__)) +#elif defined(_Ret_notnull_) /* SAL */ + #define JSON_HEDLEY_RETURNS_NON_NULL _Ret_notnull_ +#else + #define JSON_HEDLEY_RETURNS_NON_NULL +#endif + +#if defined(JSON_HEDLEY_ARRAY_PARAM) + #undef JSON_HEDLEY_ARRAY_PARAM +#endif +#if \ + defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && \ + !defined(__STDC_NO_VLA__) && \ + !defined(__cplusplus) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_TINYC_VERSION) + #define JSON_HEDLEY_ARRAY_PARAM(name) (name) +#else + #define JSON_HEDLEY_ARRAY_PARAM(name) +#endif + +#if defined(JSON_HEDLEY_IS_CONSTANT) + #undef JSON_HEDLEY_IS_CONSTANT +#endif +#if defined(JSON_HEDLEY_REQUIRE_CONSTEXPR) + #undef JSON_HEDLEY_REQUIRE_CONSTEXPR +#endif +/* JSON_HEDLEY_IS_CONSTEXPR_ is for + HEDLEY INTERNAL USE ONLY. API subject to change without notice. */ +#if defined(JSON_HEDLEY_IS_CONSTEXPR_) + #undef JSON_HEDLEY_IS_CONSTEXPR_ +#endif +#if \ + JSON_HEDLEY_HAS_BUILTIN(__builtin_constant_p) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,19) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) && !defined(__cplusplus)) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_IS_CONSTANT(expr) __builtin_constant_p(expr) +#endif +#if !defined(__cplusplus) +# if \ + JSON_HEDLEY_HAS_BUILTIN(__builtin_types_compatible_p) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,24) +#if defined(__INTPTR_TYPE__) + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0)), int*) +#else + #include + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((intptr_t) ((expr) * 0)) : (int*) 0)), int*) +#endif +# elif \ + ( \ + defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && \ + !defined(JSON_HEDLEY_SUNPRO_VERSION) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_IAR_VERSION)) || \ + (JSON_HEDLEY_HAS_EXTENSION(c_generic_selections) && !defined(JSON_HEDLEY_IAR_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,3,0) +#if defined(__INTPTR_TYPE__) + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0), int*: 1, void*: 0) +#else + #include + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((intptr_t) * 0) : (int*) 0), int*: 1, void*: 0) +#endif +# elif \ + defined(JSON_HEDLEY_GCC_VERSION) || \ + defined(JSON_HEDLEY_INTEL_VERSION) || \ + defined(JSON_HEDLEY_TINYC_VERSION) || \ + defined(JSON_HEDLEY_TI_ARMCL_VERSION) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(18,12,0) || \ + defined(JSON_HEDLEY_TI_CL2000_VERSION) || \ + defined(JSON_HEDLEY_TI_CL6X_VERSION) || \ + defined(JSON_HEDLEY_TI_CL7X_VERSION) || \ + defined(JSON_HEDLEY_TI_CLPRU_VERSION) || \ + defined(__clang__) +# define JSON_HEDLEY_IS_CONSTEXPR_(expr) ( \ + sizeof(void) != \ + sizeof(*( \ + 1 ? \ + ((void*) ((expr) * 0L) ) : \ +((struct { char v[sizeof(void) * 2]; } *) 1) \ + ) \ + ) \ + ) +# endif +#endif +#if defined(JSON_HEDLEY_IS_CONSTEXPR_) + #if !defined(JSON_HEDLEY_IS_CONSTANT) + #define JSON_HEDLEY_IS_CONSTANT(expr) JSON_HEDLEY_IS_CONSTEXPR_(expr) + #endif + #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (JSON_HEDLEY_IS_CONSTEXPR_(expr) ? (expr) : (-1)) +#else + #if !defined(JSON_HEDLEY_IS_CONSTANT) + #define JSON_HEDLEY_IS_CONSTANT(expr) (0) + #endif + #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (expr) +#endif + +#if defined(JSON_HEDLEY_BEGIN_C_DECLS) + #undef JSON_HEDLEY_BEGIN_C_DECLS +#endif +#if defined(JSON_HEDLEY_END_C_DECLS) + #undef JSON_HEDLEY_END_C_DECLS +#endif +#if defined(JSON_HEDLEY_C_DECL) + #undef JSON_HEDLEY_C_DECL +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_BEGIN_C_DECLS extern "C" { + #define JSON_HEDLEY_END_C_DECLS } + #define JSON_HEDLEY_C_DECL extern "C" +#else + #define JSON_HEDLEY_BEGIN_C_DECLS + #define JSON_HEDLEY_END_C_DECLS + #define JSON_HEDLEY_C_DECL +#endif + +#if defined(JSON_HEDLEY_STATIC_ASSERT) + #undef JSON_HEDLEY_STATIC_ASSERT +#endif +#if \ + !defined(__cplusplus) && ( \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)) || \ + (JSON_HEDLEY_HAS_FEATURE(c_static_assert) && !defined(JSON_HEDLEY_INTEL_CL_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(6,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + defined(_Static_assert) \ + ) +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) _Static_assert(expr, message) +#elif \ + (defined(__cplusplus) && (__cplusplus >= 201103L)) || \ + JSON_HEDLEY_MSVC_VERSION_CHECK(16,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(static_assert(expr, message)) +#else +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) +#endif + +#if defined(JSON_HEDLEY_NULL) + #undef JSON_HEDLEY_NULL +#endif +#if defined(__cplusplus) + #if __cplusplus >= 201103L + #define JSON_HEDLEY_NULL JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(nullptr) + #elif defined(NULL) + #define JSON_HEDLEY_NULL NULL + #else + #define JSON_HEDLEY_NULL JSON_HEDLEY_STATIC_CAST(void*, 0) + #endif +#elif defined(NULL) + #define JSON_HEDLEY_NULL NULL +#else + #define JSON_HEDLEY_NULL ((void*) 0) +#endif + +#if defined(JSON_HEDLEY_MESSAGE) + #undef JSON_HEDLEY_MESSAGE +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") +# define JSON_HEDLEY_MESSAGE(msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \ + JSON_HEDLEY_PRAGMA(message msg) \ + JSON_HEDLEY_DIAGNOSTIC_POP +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message msg) +#elif JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(_CRI message msg) +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#else +# define JSON_HEDLEY_MESSAGE(msg) +#endif + +#if defined(JSON_HEDLEY_WARNING) + #undef JSON_HEDLEY_WARNING +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") +# define JSON_HEDLEY_WARNING(msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \ + JSON_HEDLEY_PRAGMA(clang warning msg) \ + JSON_HEDLEY_DIAGNOSTIC_POP +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,8,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(GCC warning msg) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#else +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_MESSAGE(msg) +#endif + +#if defined(JSON_HEDLEY_REQUIRE) + #undef JSON_HEDLEY_REQUIRE +#endif +#if defined(JSON_HEDLEY_REQUIRE_MSG) + #undef JSON_HEDLEY_REQUIRE_MSG +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(diagnose_if) +# if JSON_HEDLEY_HAS_WARNING("-Wgcc-compat") +# define JSON_HEDLEY_REQUIRE(expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \ + __attribute__((diagnose_if(!(expr), #expr, "error"))) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \ + __attribute__((diagnose_if(!(expr), msg, "error"))) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_REQUIRE(expr) __attribute__((diagnose_if(!(expr), #expr, "error"))) +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) __attribute__((diagnose_if(!(expr), msg, "error"))) +# endif +#else +# define JSON_HEDLEY_REQUIRE(expr) +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) +#endif + +#if defined(JSON_HEDLEY_FLAGS) + #undef JSON_HEDLEY_FLAGS +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(flag_enum) && (!defined(__cplusplus) || JSON_HEDLEY_HAS_WARNING("-Wbitfield-enum-conversion")) + #define JSON_HEDLEY_FLAGS __attribute__((__flag_enum__)) +#else + #define JSON_HEDLEY_FLAGS +#endif + +#if defined(JSON_HEDLEY_FLAGS_CAST) + #undef JSON_HEDLEY_FLAGS_CAST +#endif +#if JSON_HEDLEY_INTEL_VERSION_CHECK(19,0,0) +# define JSON_HEDLEY_FLAGS_CAST(T, expr) (__extension__ ({ \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("warning(disable:188)") \ + ((T) (expr)); \ + JSON_HEDLEY_DIAGNOSTIC_POP \ + })) +#else +# define JSON_HEDLEY_FLAGS_CAST(T, expr) JSON_HEDLEY_STATIC_CAST(T, expr) +#endif + +#if defined(JSON_HEDLEY_EMPTY_BASES) + #undef JSON_HEDLEY_EMPTY_BASES +#endif +#if \ + (JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,23918) && !JSON_HEDLEY_MSVC_VERSION_CHECK(20,0,0)) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_EMPTY_BASES __declspec(empty_bases) +#else + #define JSON_HEDLEY_EMPTY_BASES +#endif + +/* Remaining macros are deprecated. */ + +#if defined(JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK) + #undef JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK +#endif +#if defined(__clang__) + #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) (0) +#else + #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_CLANG_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_BUILTIN) + #undef JSON_HEDLEY_CLANG_HAS_BUILTIN +#endif +#define JSON_HEDLEY_CLANG_HAS_BUILTIN(builtin) JSON_HEDLEY_HAS_BUILTIN(builtin) + +#if defined(JSON_HEDLEY_CLANG_HAS_FEATURE) + #undef JSON_HEDLEY_CLANG_HAS_FEATURE +#endif +#define JSON_HEDLEY_CLANG_HAS_FEATURE(feature) JSON_HEDLEY_HAS_FEATURE(feature) + +#if defined(JSON_HEDLEY_CLANG_HAS_EXTENSION) + #undef JSON_HEDLEY_CLANG_HAS_EXTENSION +#endif +#define JSON_HEDLEY_CLANG_HAS_EXTENSION(extension) JSON_HEDLEY_HAS_EXTENSION(extension) + +#if defined(JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_DECLSPEC_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_WARNING) + #undef JSON_HEDLEY_CLANG_HAS_WARNING +#endif +#define JSON_HEDLEY_CLANG_HAS_WARNING(warning) JSON_HEDLEY_HAS_WARNING(warning) + +#endif /* !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < X) */ + + +// This file contains all internal macro definitions (except those affecting ABI) +// You MUST include macro_unscope.hpp at the end of json.hpp to undef all of them + +// #include + + +// exclude unsupported compilers +#if !defined(JSON_SKIP_UNSUPPORTED_COMPILER_CHECK) + #if defined(__clang__) + #if (__clang_major__ * 10000 + __clang_minor__ * 100 + __clang_patchlevel__) < 30400 + #error "unsupported Clang version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #elif defined(__GNUC__) && !(defined(__ICC) || defined(__INTEL_COMPILER)) + #if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) < 40800 + #error "unsupported GCC version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #endif +#endif + +// C++ language standard detection +// if the user manually specified the used c++ version this is skipped +#if !defined(JSON_HAS_CPP_20) && !defined(JSON_HAS_CPP_17) && !defined(JSON_HAS_CPP_14) && !defined(JSON_HAS_CPP_11) + #if (defined(__cplusplus) && __cplusplus >= 202002L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) + #define JSON_HAS_CPP_20 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 + #elif (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 + #elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1) + #define JSON_HAS_CPP_14 + #endif + // the cpp 11 flag is always specified because it is the minimal required version + #define JSON_HAS_CPP_11 +#endif + +#ifdef __has_include + #if __has_include() + #include + #endif +#endif + +#if !defined(JSON_HAS_FILESYSTEM) && !defined(JSON_HAS_EXPERIMENTAL_FILESYSTEM) + #ifdef JSON_HAS_CPP_17 + #if defined(__cpp_lib_filesystem) + #define JSON_HAS_FILESYSTEM 1 + #elif defined(__cpp_lib_experimental_filesystem) + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 + #elif !defined(__has_include) + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 + #elif __has_include() + #define JSON_HAS_FILESYSTEM 1 + #elif __has_include() + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 + #endif + + // std::filesystem does not work on MinGW GCC 8: https://sourceforge.net/p/mingw-w64/bugs/737/ + #if defined(__MINGW32__) && defined(__GNUC__) && __GNUC__ == 8 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before GCC 8: https://en.cppreference.com/w/cpp/compiler_support + #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 8 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before Clang 7: https://en.cppreference.com/w/cpp/compiler_support + #if defined(__clang_major__) && __clang_major__ < 7 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before MSVC 19.14: https://en.cppreference.com/w/cpp/compiler_support + #if defined(_MSC_VER) && _MSC_VER < 1914 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before iOS 13 + #if defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < 130000 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before macOS Catalina + #if defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + #endif +#endif + +#ifndef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 0 +#endif + +#ifndef JSON_HAS_FILESYSTEM + #define JSON_HAS_FILESYSTEM 0 +#endif + +#ifndef JSON_HAS_THREE_WAY_COMPARISON + #if defined(__cpp_impl_three_way_comparison) && __cpp_impl_three_way_comparison >= 201907L \ + && defined(__cpp_lib_three_way_comparison) && __cpp_lib_three_way_comparison >= 201907L + #define JSON_HAS_THREE_WAY_COMPARISON 1 + #else + #define JSON_HAS_THREE_WAY_COMPARISON 0 + #endif +#endif + +#ifndef JSON_HAS_RANGES + // ranges header shipping in GCC 11.1.0 (released 2021-04-27) has syntax error + #if defined(__GLIBCXX__) && __GLIBCXX__ == 20210427 + #define JSON_HAS_RANGES 0 + #elif defined(__cpp_lib_ranges) + #define JSON_HAS_RANGES 1 + #else + #define JSON_HAS_RANGES 0 + #endif +#endif + +#ifndef JSON_HAS_STATIC_RTTI + #if !defined(_HAS_STATIC_RTTI) || _HAS_STATIC_RTTI != 0 + #define JSON_HAS_STATIC_RTTI 1 + #else + #define JSON_HAS_STATIC_RTTI 0 + #endif +#endif + +#ifdef JSON_HAS_CPP_17 + #define JSON_INLINE_VARIABLE inline +#else + #define JSON_INLINE_VARIABLE +#endif + +#if JSON_HEDLEY_HAS_ATTRIBUTE(no_unique_address) + #define JSON_NO_UNIQUE_ADDRESS [[no_unique_address]] +#else + #define JSON_NO_UNIQUE_ADDRESS +#endif + +// disable documentation warnings on clang +#if defined(__clang__) + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdocumentation" + #pragma clang diagnostic ignored "-Wdocumentation-unknown-command" +#endif + +// allow disabling exceptions +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) + #define JSON_THROW(exception) throw exception + #define JSON_TRY try + #define JSON_CATCH(exception) catch(exception) + #define JSON_INTERNAL_CATCH(exception) catch(exception) +#else + #include + #define JSON_THROW(exception) std::abort() + #define JSON_TRY if(true) + #define JSON_CATCH(exception) if(false) + #define JSON_INTERNAL_CATCH(exception) if(false) +#endif + +// override exception macros +#if defined(JSON_THROW_USER) + #undef JSON_THROW + #define JSON_THROW JSON_THROW_USER +#endif +#if defined(JSON_TRY_USER) + #undef JSON_TRY + #define JSON_TRY JSON_TRY_USER +#endif +#if defined(JSON_CATCH_USER) + #undef JSON_CATCH + #define JSON_CATCH JSON_CATCH_USER + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_CATCH_USER +#endif +#if defined(JSON_INTERNAL_CATCH_USER) + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_INTERNAL_CATCH_USER +#endif + +// allow overriding assert +#if !defined(JSON_ASSERT) + #include // assert + #define JSON_ASSERT(x) assert(x) +#endif + +// allow to access some private functions (needed by the test suite) +#if defined(JSON_TESTS_PRIVATE) + #define JSON_PRIVATE_UNLESS_TESTED public +#else + #define JSON_PRIVATE_UNLESS_TESTED private +#endif + +/*! +@brief macro to briefly define a mapping between an enum and JSON +@def NLOHMANN_JSON_SERIALIZE_ENUM +@since version 3.4.0 +*/ +#define NLOHMANN_JSON_SERIALIZE_ENUM(ENUM_TYPE, ...) \ + template \ + inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \ + { \ + /* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + /* NOLINTNEXTLINE(modernize-avoid-c-arrays) we don't want to depend on */ \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [e](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.first == e; \ + }); \ + j = ((it != std::end(m)) ? it : std::begin(m))->second; \ + } \ + template \ + inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ + { \ + /* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + /* NOLINTNEXTLINE(modernize-avoid-c-arrays) we don't want to depend on */ \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [&j](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.second == j; \ + }); \ + e = ((it != std::end(m)) ? it : std::begin(m))->first; \ + } + +// Ugly macros to avoid uglier copy-paste when specializing basic_json. They +// may be removed in the future once the class is split. + +#define NLOHMANN_BASIC_JSON_TPL_DECLARATION \ + template class ObjectType, \ + template class ArrayType, \ + class StringType, class BooleanType, class NumberIntegerType, \ + class NumberUnsignedType, class NumberFloatType, \ + template class AllocatorType, \ + template class JSONSerializer, \ + class BinaryType, \ + class CustomBaseClass> + +#define NLOHMANN_BASIC_JSON_TPL \ + basic_json + +// Macros to simplify conversion from/to types + +#define NLOHMANN_JSON_EXPAND( x ) x +#define NLOHMANN_JSON_GET_MACRO(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, NAME,...) NAME +#define NLOHMANN_JSON_PASTE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_GET_MACRO(__VA_ARGS__, \ + NLOHMANN_JSON_PASTE64, \ + NLOHMANN_JSON_PASTE63, \ + NLOHMANN_JSON_PASTE62, \ + NLOHMANN_JSON_PASTE61, \ + NLOHMANN_JSON_PASTE60, \ + NLOHMANN_JSON_PASTE59, \ + NLOHMANN_JSON_PASTE58, \ + NLOHMANN_JSON_PASTE57, \ + NLOHMANN_JSON_PASTE56, \ + NLOHMANN_JSON_PASTE55, \ + NLOHMANN_JSON_PASTE54, \ + NLOHMANN_JSON_PASTE53, \ + NLOHMANN_JSON_PASTE52, \ + NLOHMANN_JSON_PASTE51, \ + NLOHMANN_JSON_PASTE50, \ + NLOHMANN_JSON_PASTE49, \ + NLOHMANN_JSON_PASTE48, \ + NLOHMANN_JSON_PASTE47, \ + NLOHMANN_JSON_PASTE46, \ + NLOHMANN_JSON_PASTE45, \ + NLOHMANN_JSON_PASTE44, \ + NLOHMANN_JSON_PASTE43, \ + NLOHMANN_JSON_PASTE42, \ + NLOHMANN_JSON_PASTE41, \ + NLOHMANN_JSON_PASTE40, \ + NLOHMANN_JSON_PASTE39, \ + NLOHMANN_JSON_PASTE38, \ + NLOHMANN_JSON_PASTE37, \ + NLOHMANN_JSON_PASTE36, \ + NLOHMANN_JSON_PASTE35, \ + NLOHMANN_JSON_PASTE34, \ + NLOHMANN_JSON_PASTE33, \ + NLOHMANN_JSON_PASTE32, \ + NLOHMANN_JSON_PASTE31, \ + NLOHMANN_JSON_PASTE30, \ + NLOHMANN_JSON_PASTE29, \ + NLOHMANN_JSON_PASTE28, \ + NLOHMANN_JSON_PASTE27, \ + NLOHMANN_JSON_PASTE26, \ + NLOHMANN_JSON_PASTE25, \ + NLOHMANN_JSON_PASTE24, \ + NLOHMANN_JSON_PASTE23, \ + NLOHMANN_JSON_PASTE22, \ + NLOHMANN_JSON_PASTE21, \ + NLOHMANN_JSON_PASTE20, \ + NLOHMANN_JSON_PASTE19, \ + NLOHMANN_JSON_PASTE18, \ + NLOHMANN_JSON_PASTE17, \ + NLOHMANN_JSON_PASTE16, \ + NLOHMANN_JSON_PASTE15, \ + NLOHMANN_JSON_PASTE14, \ + NLOHMANN_JSON_PASTE13, \ + NLOHMANN_JSON_PASTE12, \ + NLOHMANN_JSON_PASTE11, \ + NLOHMANN_JSON_PASTE10, \ + NLOHMANN_JSON_PASTE9, \ + NLOHMANN_JSON_PASTE8, \ + NLOHMANN_JSON_PASTE7, \ + NLOHMANN_JSON_PASTE6, \ + NLOHMANN_JSON_PASTE5, \ + NLOHMANN_JSON_PASTE4, \ + NLOHMANN_JSON_PASTE3, \ + NLOHMANN_JSON_PASTE2, \ + NLOHMANN_JSON_PASTE1)(__VA_ARGS__)) +#define NLOHMANN_JSON_PASTE2(func, v1) func(v1) +#define NLOHMANN_JSON_PASTE3(func, v1, v2) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE2(func, v2) +#define NLOHMANN_JSON_PASTE4(func, v1, v2, v3) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE3(func, v2, v3) +#define NLOHMANN_JSON_PASTE5(func, v1, v2, v3, v4) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE4(func, v2, v3, v4) +#define NLOHMANN_JSON_PASTE6(func, v1, v2, v3, v4, v5) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE5(func, v2, v3, v4, v5) +#define NLOHMANN_JSON_PASTE7(func, v1, v2, v3, v4, v5, v6) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE6(func, v2, v3, v4, v5, v6) +#define NLOHMANN_JSON_PASTE8(func, v1, v2, v3, v4, v5, v6, v7) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE7(func, v2, v3, v4, v5, v6, v7) +#define NLOHMANN_JSON_PASTE9(func, v1, v2, v3, v4, v5, v6, v7, v8) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE8(func, v2, v3, v4, v5, v6, v7, v8) +#define NLOHMANN_JSON_PASTE10(func, v1, v2, v3, v4, v5, v6, v7, v8, v9) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE9(func, v2, v3, v4, v5, v6, v7, v8, v9) +#define NLOHMANN_JSON_PASTE11(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE10(func, v2, v3, v4, v5, v6, v7, v8, v9, v10) +#define NLOHMANN_JSON_PASTE12(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE11(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) +#define NLOHMANN_JSON_PASTE13(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE12(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) +#define NLOHMANN_JSON_PASTE14(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE13(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) +#define NLOHMANN_JSON_PASTE15(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE14(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) +#define NLOHMANN_JSON_PASTE16(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE15(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) +#define NLOHMANN_JSON_PASTE17(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE16(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) +#define NLOHMANN_JSON_PASTE18(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE17(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) +#define NLOHMANN_JSON_PASTE19(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE18(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) +#define NLOHMANN_JSON_PASTE20(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE19(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) +#define NLOHMANN_JSON_PASTE21(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE20(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) +#define NLOHMANN_JSON_PASTE22(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE21(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) +#define NLOHMANN_JSON_PASTE23(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE22(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) +#define NLOHMANN_JSON_PASTE24(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE23(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) +#define NLOHMANN_JSON_PASTE25(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE24(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) +#define NLOHMANN_JSON_PASTE26(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE25(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) +#define NLOHMANN_JSON_PASTE27(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE26(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) +#define NLOHMANN_JSON_PASTE28(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE27(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) +#define NLOHMANN_JSON_PASTE29(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE28(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) +#define NLOHMANN_JSON_PASTE30(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE29(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) +#define NLOHMANN_JSON_PASTE31(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE30(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) +#define NLOHMANN_JSON_PASTE32(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE31(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) +#define NLOHMANN_JSON_PASTE33(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE32(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) +#define NLOHMANN_JSON_PASTE34(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE33(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) +#define NLOHMANN_JSON_PASTE35(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE34(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) +#define NLOHMANN_JSON_PASTE36(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE35(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) +#define NLOHMANN_JSON_PASTE37(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE36(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) +#define NLOHMANN_JSON_PASTE38(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE37(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) +#define NLOHMANN_JSON_PASTE39(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE38(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) +#define NLOHMANN_JSON_PASTE40(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE39(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) +#define NLOHMANN_JSON_PASTE41(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE40(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) +#define NLOHMANN_JSON_PASTE42(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE41(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) +#define NLOHMANN_JSON_PASTE43(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE42(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) +#define NLOHMANN_JSON_PASTE44(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE43(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) +#define NLOHMANN_JSON_PASTE45(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE44(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) +#define NLOHMANN_JSON_PASTE46(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE45(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) +#define NLOHMANN_JSON_PASTE47(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE46(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) +#define NLOHMANN_JSON_PASTE48(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE47(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) +#define NLOHMANN_JSON_PASTE49(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE48(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) +#define NLOHMANN_JSON_PASTE50(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE49(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) +#define NLOHMANN_JSON_PASTE51(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE50(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) +#define NLOHMANN_JSON_PASTE52(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE51(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) +#define NLOHMANN_JSON_PASTE53(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE52(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) +#define NLOHMANN_JSON_PASTE54(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE53(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) +#define NLOHMANN_JSON_PASTE55(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE54(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) +#define NLOHMANN_JSON_PASTE56(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE55(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) +#define NLOHMANN_JSON_PASTE57(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE56(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) +#define NLOHMANN_JSON_PASTE58(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE57(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) +#define NLOHMANN_JSON_PASTE59(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE58(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) +#define NLOHMANN_JSON_PASTE60(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE59(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) +#define NLOHMANN_JSON_PASTE61(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE60(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) +#define NLOHMANN_JSON_PASTE62(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE61(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) +#define NLOHMANN_JSON_PASTE63(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE62(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) +#define NLOHMANN_JSON_PASTE64(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE63(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) + +#define NLOHMANN_JSON_TO(v1) nlohmann_json_j[#v1] = nlohmann_json_t.v1; +#define NLOHMANN_JSON_FROM(v1) nlohmann_json_j.at(#v1).get_to(nlohmann_json_t.v1); +#define NLOHMANN_JSON_FROM_WITH_DEFAULT(v1) nlohmann_json_t.v1 = !nlohmann_json_j.is_null() ? nlohmann_json_j.value(#v1, nlohmann_json_default_obj.v1) : nlohmann_json_default_obj.v1; + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_INTRUSIVE +@since version 3.9.0 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_type_intrusive/ +*/ +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE(Type, ...) \ + template::value, int> = 0> \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT +@since version 3.11.0 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_type_intrusive/ +*/ +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Type, ...) \ + template::value, int> = 0> \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE +@since version 3.11.3 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_type_intrusive/ +*/ +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(Type, ...) \ + template::value, int> = 0> \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE +@since version 3.9.0 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_type_non_intrusive/ +*/ +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Type, ...) \ + template::value, int> = 0> \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT +@since version 3.11.0 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_type_non_intrusive/ +*/ +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Type, ...) \ + template::value, int> = 0> \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE +@since version 3.11.3 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_type_non_intrusive/ +*/ +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE(Type, ...) \ + template::value, int> = 0> \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE +@since version 3.11.x +@sa https://json.nlohmann.me/api/macros/nlohmann_define_derived_type/ +*/ +#define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE(Type, BaseType, ...) \ + template::value, int> = 0> \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT +@since version 3.11.x +@sa https://json.nlohmann.me/api/macros/nlohmann_define_derived_type/ +*/ +#define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT(Type, BaseType, ...) \ + template::value, int> = 0> \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_ONLY_SERIALIZE +@since version 3.11.x +@sa https://json.nlohmann.me/api/macros/nlohmann_define_derived_type/ +*/ +#define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_ONLY_SERIALIZE(Type, BaseType, ...) \ + template::value, int> = 0> \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE +@since version 3.11.x +@sa https://json.nlohmann.me/api/macros/nlohmann_define_derived_type/ +*/ +#define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE(Type, BaseType, ...) \ + template::value, int> = 0> \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT +@since version 3.11.x +@sa https://json.nlohmann.me/api/macros/nlohmann_define_derived_type/ +*/ +#define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Type, BaseType, ...) \ + template::value, int> = 0> \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE +@since version 3.11.x +@sa https://json.nlohmann.me/api/macros/nlohmann_define_derived_type/ +*/ +#define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE(Type, BaseType, ...) \ + template::value, int> = 0> \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + +// inspired from https://stackoverflow.com/a/26745591 +// allows to call any std function as if (e.g. with begin): +// using std::begin; begin(x); +// +// it allows using the detected idiom to retrieve the return type +// of such an expression +#define NLOHMANN_CAN_CALL_STD_FUNC_IMPL(std_name) \ + namespace detail { \ + using std::std_name; \ + \ + template \ + using result_of_##std_name = decltype(std_name(std::declval()...)); \ + } \ + \ + namespace detail2 { \ + struct std_name##_tag \ + { \ + }; \ + \ + template \ + std_name##_tag std_name(T&&...); \ + \ + template \ + using result_of_##std_name = decltype(std_name(std::declval()...)); \ + \ + template \ + struct would_call_std_##std_name \ + { \ + static constexpr auto const value = ::nlohmann::detail:: \ + is_detected_exact::value; \ + }; \ + } /* namespace detail2 */ \ + \ + template \ + struct would_call_std_##std_name : detail2::would_call_std_##std_name \ + { \ + } + +#ifndef JSON_USE_IMPLICIT_CONVERSIONS + #define JSON_USE_IMPLICIT_CONVERSIONS 1 +#endif + +#if JSON_USE_IMPLICIT_CONVERSIONS + #define JSON_EXPLICIT +#else + #define JSON_EXPLICIT explicit +#endif + +#ifndef JSON_DISABLE_ENUM_SERIALIZATION + #define JSON_DISABLE_ENUM_SERIALIZATION 0 +#endif + +#ifndef JSON_USE_GLOBAL_UDLS + #define JSON_USE_GLOBAL_UDLS 1 +#endif + +#if JSON_HAS_THREE_WAY_COMPARISON + #include // partial_ordering +#endif + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/////////////////////////// +// JSON type enumeration // +/////////////////////////// + +/*! +@brief the JSON type enumeration + +This enumeration collects the different JSON types. It is internally used to +distinguish the stored values, and the functions @ref basic_json::is_null(), +@ref basic_json::is_object(), @ref basic_json::is_array(), +@ref basic_json::is_string(), @ref basic_json::is_boolean(), +@ref basic_json::is_number() (with @ref basic_json::is_number_integer(), +@ref basic_json::is_number_unsigned(), and @ref basic_json::is_number_float()), +@ref basic_json::is_discarded(), @ref basic_json::is_primitive(), and +@ref basic_json::is_structured() rely on it. + +@note There are three enumeration entries (number_integer, number_unsigned, and +number_float), because the library distinguishes these three types for numbers: +@ref basic_json::number_unsigned_t is used for unsigned integers, +@ref basic_json::number_integer_t is used for signed integers, and +@ref basic_json::number_float_t is used for floating-point numbers or to +approximate integers which do not fit in the limits of their respective type. + +@sa see @ref basic_json::basic_json(const value_t value_type) -- create a JSON +value with the default value for a given type + +@since version 1.0.0 +*/ +enum class value_t : std::uint8_t +{ + null, ///< null value + object, ///< object (unordered set of name/value pairs) + array, ///< array (ordered collection of values) + string, ///< string value + boolean, ///< boolean value + number_integer, ///< number value (signed integer) + number_unsigned, ///< number value (unsigned integer) + number_float, ///< number value (floating-point) + binary, ///< binary array (ordered collection of bytes) + discarded ///< discarded by the parser callback function +}; + +/*! +@brief comparison operator for JSON types + +Returns an ordering that is similar to Python: +- order: null < boolean < number < object < array < string < binary +- furthermore, each type is not smaller than itself +- discarded values are not comparable +- binary is represented as a b"" string in python and directly comparable to a + string; however, making a binary array directly comparable with a string would + be surprising behavior in a JSON file. + +@since version 1.0.0 +*/ +#if JSON_HAS_THREE_WAY_COMPARISON + inline std::partial_ordering operator<=>(const value_t lhs, const value_t rhs) noexcept // *NOPAD* +#else + inline bool operator<(const value_t lhs, const value_t rhs) noexcept +#endif +{ + static constexpr std::array order = {{ + 0 /* null */, 3 /* object */, 4 /* array */, 5 /* string */, + 1 /* boolean */, 2 /* integer */, 2 /* unsigned */, 2 /* float */, + 6 /* binary */ + } + }; + + const auto l_index = static_cast(lhs); + const auto r_index = static_cast(rhs); +#if JSON_HAS_THREE_WAY_COMPARISON + if (l_index < order.size() && r_index < order.size()) + { + return order[l_index] <=> order[r_index]; // *NOPAD* + } + return std::partial_ordering::unordered; +#else + return l_index < order.size() && r_index < order.size() && order[l_index] < order[r_index]; +#endif +} + +// GCC selects the built-in operator< over an operator rewritten from +// a user-defined spaceship operator +// Clang, MSVC, and ICC select the rewritten candidate +// (see GCC bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105200) +#if JSON_HAS_THREE_WAY_COMPARISON && defined(__GNUC__) +inline bool operator<(const value_t lhs, const value_t rhs) noexcept +{ + return std::is_lt(lhs <=> rhs); // *NOPAD* +} +#endif + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/*! +@brief replace all occurrences of a substring by another string + +@param[in,out] s the string to manipulate; changed so that all + occurrences of @a f are replaced with @a t +@param[in] f the substring to replace with @a t +@param[in] t the string to replace @a f + +@pre The search string @a f must not be empty. **This precondition is +enforced with an assertion.** + +@since version 2.0.0 +*/ +template +inline void replace_substring(StringType& s, const StringType& f, + const StringType& t) +{ + JSON_ASSERT(!f.empty()); + for (auto pos = s.find(f); // find first occurrence of f + pos != StringType::npos; // make sure f was found + s.replace(pos, f.size(), t), // replace with t, and + pos = s.find(f, pos + t.size())) // find next occurrence of f + {} +} + +/*! + * @brief string escaping as described in RFC 6901 (Sect. 4) + * @param[in] s string to escape + * @return escaped string + * + * Note the order of escaping "~" to "~0" and "/" to "~1" is important. + */ +template +inline StringType escape(StringType s) +{ + replace_substring(s, StringType{"~"}, StringType{"~0"}); + replace_substring(s, StringType{"/"}, StringType{"~1"}); + return s; +} + +/*! + * @brief string unescaping as described in RFC 6901 (Sect. 4) + * @param[in] s string to unescape + * @return unescaped string + * + * Note the order of escaping "~1" to "/" and "~0" to "~" is important. + */ +template +static void unescape(StringType& s) +{ + replace_substring(s, StringType{"~1"}, StringType{"/"}); + replace_substring(s, StringType{"~0"}, StringType{"~"}); +} + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // size_t + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/// struct to capture the start position of the current token +struct position_t +{ + /// the total number of characters read + std::size_t chars_read_total = 0; + /// the number of characters read in the current line + std::size_t chars_read_current_line = 0; + /// the number of lines read + std::size_t lines_read = 0; + + /// conversion to size_t to preserve SAX interface + constexpr operator size_t() const + { + return chars_read_total; + } +}; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-FileCopyrightText: 2018 The Abseil Authors +// SPDX-License-Identifier: MIT + + + +#include // array +#include // size_t +#include // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type +#include // index_sequence, make_index_sequence, index_sequence_for + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +template +using uncvref_t = typename std::remove_cv::type>::type; + +#ifdef JSON_HAS_CPP_14 + +// the following utilities are natively available in C++14 +using std::enable_if_t; +using std::index_sequence; +using std::make_index_sequence; +using std::index_sequence_for; + +#else + +// alias templates to reduce boilerplate +template +using enable_if_t = typename std::enable_if::type; + +// The following code is taken from https://github.com/abseil/abseil-cpp/blob/10cb35e459f5ecca5b2ff107635da0bfa41011b4/absl/utility/utility.h +// which is part of Google Abseil (https://github.com/abseil/abseil-cpp), licensed under the Apache License 2.0. + +//// START OF CODE FROM GOOGLE ABSEIL + +// integer_sequence +// +// Class template representing a compile-time integer sequence. An instantiation +// of `integer_sequence` has a sequence of integers encoded in its +// type through its template arguments (which is a common need when +// working with C++11 variadic templates). `absl::integer_sequence` is designed +// to be a drop-in replacement for C++14's `std::integer_sequence`. +// +// Example: +// +// template< class T, T... Ints > +// void user_function(integer_sequence); +// +// int main() +// { +// // user_function's `T` will be deduced to `int` and `Ints...` +// // will be deduced to `0, 1, 2, 3, 4`. +// user_function(make_integer_sequence()); +// } +template +struct integer_sequence +{ + using value_type = T; + static constexpr std::size_t size() noexcept + { + return sizeof...(Ints); + } +}; + +// index_sequence +// +// A helper template for an `integer_sequence` of `size_t`, +// `absl::index_sequence` is designed to be a drop-in replacement for C++14's +// `std::index_sequence`. +template +using index_sequence = integer_sequence; + +namespace utility_internal +{ + +template +struct Extend; + +// Note that SeqSize == sizeof...(Ints). It's passed explicitly for efficiency. +template +struct Extend, SeqSize, 0> +{ + using type = integer_sequence < T, Ints..., (Ints + SeqSize)... >; +}; + +template +struct Extend, SeqSize, 1> +{ + using type = integer_sequence < T, Ints..., (Ints + SeqSize)..., 2 * SeqSize >; +}; + +// Recursion helper for 'make_integer_sequence'. +// 'Gen::type' is an alias for 'integer_sequence'. +template +struct Gen +{ + using type = + typename Extend < typename Gen < T, N / 2 >::type, N / 2, N % 2 >::type; +}; + +template +struct Gen +{ + using type = integer_sequence; +}; + +} // namespace utility_internal + +// Compile-time sequences of integers + +// make_integer_sequence +// +// This template alias is equivalent to +// `integer_sequence`, and is designed to be a drop-in +// replacement for C++14's `std::make_integer_sequence`. +template +using make_integer_sequence = typename utility_internal::Gen::type; + +// make_index_sequence +// +// This template alias is equivalent to `index_sequence<0, 1, ..., N-1>`, +// and is designed to be a drop-in replacement for C++14's +// `std::make_index_sequence`. +template +using make_index_sequence = make_integer_sequence; + +// index_sequence_for +// +// Converts a typename pack into an index sequence of the same length, and +// is designed to be a drop-in replacement for C++14's +// `std::index_sequence_for()` +template +using index_sequence_for = make_index_sequence; + +//// END OF CODE FROM GOOGLE ABSEIL + +#endif + +// dispatch utility (taken from ranges-v3) +template struct priority_tag : priority_tag < N - 1 > {}; +template<> struct priority_tag<0> {}; + +// taken from ranges-v3 +template +struct static_const +{ + static JSON_INLINE_VARIABLE constexpr T value{}; +}; + +#ifndef JSON_HAS_CPP_17 + template + constexpr T static_const::value; +#endif + +template +constexpr std::array make_array(Args&& ... args) +{ + return std::array {{static_cast(std::forward(args))...}}; +} + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // numeric_limits +#include // char_traits +#include // tuple +#include // false_type, is_constructible, is_integral, is_same, true_type +#include // declval + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // random_access_iterator_tag + +// #include + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +template +struct iterator_types {}; + +template +struct iterator_types < + It, + void_t> +{ + using difference_type = typename It::difference_type; + using value_type = typename It::value_type; + using pointer = typename It::pointer; + using reference = typename It::reference; + using iterator_category = typename It::iterator_category; +}; + +// This is required as some compilers implement std::iterator_traits in a way that +// doesn't work with SFINAE. See https://github.com/nlohmann/json/issues/1341. +template +struct iterator_traits +{ +}; + +template +struct iterator_traits < T, enable_if_t < !std::is_pointer::value >> + : iterator_types +{ +}; + +template +struct iterator_traits::value>> +{ + using iterator_category = std::random_access_iterator_tag; + using value_type = T; + using difference_type = ptrdiff_t; + using pointer = T*; + using reference = T&; +}; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +NLOHMANN_CAN_CALL_STD_FUNC_IMPL(begin); + +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +NLOHMANN_CAN_CALL_STD_FUNC_IMPL(end); + +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + +#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_ + #define INCLUDE_NLOHMANN_JSON_FWD_HPP_ + + #include // int64_t, uint64_t + #include // map + #include // allocator + #include // string + #include // vector + + // #include + + + /*! + @brief namespace for Niels Lohmann + @see https://github.com/nlohmann + @since version 1.0.0 + */ + NLOHMANN_JSON_NAMESPACE_BEGIN + + /*! + @brief default JSONSerializer template argument + + This serializer ignores the template arguments and uses ADL + ([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl)) + for serialization. + */ + template + struct adl_serializer; + + /// a class to store JSON values + /// @sa https://json.nlohmann.me/api/basic_json/ + template class ObjectType = + std::map, + template class ArrayType = std::vector, + class StringType = std::string, class BooleanType = bool, + class NumberIntegerType = std::int64_t, + class NumberUnsignedType = std::uint64_t, + class NumberFloatType = double, + template class AllocatorType = std::allocator, + template class JSONSerializer = + adl_serializer, + class BinaryType = std::vector, // cppcheck-suppress syntaxError + class CustomBaseClass = void> + class basic_json; + + /// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document + /// @sa https://json.nlohmann.me/api/json_pointer/ + template + class json_pointer; + + /*! + @brief default specialization + @sa https://json.nlohmann.me/api/json/ + */ + using json = basic_json<>; + + /// @brief a minimal map-like container that preserves insertion order + /// @sa https://json.nlohmann.me/api/ordered_map/ + template + struct ordered_map; + + /// @brief specialization that maintains the insertion order of object keys + /// @sa https://json.nlohmann.me/api/ordered_json/ + using ordered_json = basic_json; + + NLOHMANN_JSON_NAMESPACE_END + +#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_ + + +NLOHMANN_JSON_NAMESPACE_BEGIN +/*! +@brief detail namespace with internal helper functions + +This namespace collects functions that should not be exposed, +implementations of some @ref basic_json methods, and meta-programming helpers. + +@since version 2.1.0 +*/ +namespace detail +{ + +///////////// +// helpers // +///////////// + +// Note to maintainers: +// +// Every trait in this file expects a non CV-qualified type. +// The only exceptions are in the 'aliases for detected' section +// (i.e. those of the form: decltype(T::member_function(std::declval()))) +// +// In this case, T has to be properly CV-qualified to constraint the function arguments +// (e.g. to_json(BasicJsonType&, const T&)) + +template struct is_basic_json : std::false_type {}; + +NLOHMANN_BASIC_JSON_TPL_DECLARATION +struct is_basic_json : std::true_type {}; + +// used by exceptions create() member functions +// true_type for pointer to possibly cv-qualified basic_json or std::nullptr_t +// false_type otherwise +template +struct is_basic_json_context : + std::integral_constant < bool, + is_basic_json::type>::type>::value + || std::is_same::value > +{}; + +////////////////////// +// json_ref helpers // +////////////////////// + +template +class json_ref; + +template +struct is_json_ref : std::false_type {}; + +template +struct is_json_ref> : std::true_type {}; + +////////////////////////// +// aliases for detected // +////////////////////////// + +template +using mapped_type_t = typename T::mapped_type; + +template +using key_type_t = typename T::key_type; + +template +using value_type_t = typename T::value_type; + +template +using difference_type_t = typename T::difference_type; + +template +using pointer_t = typename T::pointer; + +template +using reference_t = typename T::reference; + +template +using iterator_category_t = typename T::iterator_category; + +template +using to_json_function = decltype(T::to_json(std::declval()...)); + +template +using from_json_function = decltype(T::from_json(std::declval()...)); + +template +using get_template_function = decltype(std::declval().template get()); + +// trait checking if JSONSerializer::from_json(json const&, udt&) exists +template +struct has_from_json : std::false_type {}; + +// trait checking if j.get is valid +// use this trait instead of std::is_constructible or std::is_convertible, +// both rely on, or make use of implicit conversions, and thus fail when T +// has several constructors/operator= (see https://github.com/nlohmann/json/issues/958) +template +struct is_getable +{ + static constexpr bool value = is_detected::value; +}; + +template +struct has_from_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + +// This trait checks if JSONSerializer::from_json(json const&) exists +// this overload is used for non-default-constructible user-defined-types +template +struct has_non_default_from_json : std::false_type {}; + +template +struct has_non_default_from_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + +// This trait checks if BasicJsonType::json_serializer::to_json exists +// Do not evaluate the trait when T is a basic_json type, to avoid template instantiation infinite recursion. +template +struct has_to_json : std::false_type {}; + +template +struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + +template +using detect_key_compare = typename T::key_compare; + +template +struct has_key_compare : std::integral_constant::value> {}; + +// obtains the actual object key comparator +template +struct actual_object_comparator +{ + using object_t = typename BasicJsonType::object_t; + using object_comparator_t = typename BasicJsonType::default_object_comparator_t; + using type = typename std::conditional < has_key_compare::value, + typename object_t::key_compare, object_comparator_t>::type; +}; + +template +using actual_object_comparator_t = typename actual_object_comparator::type; + +///////////////// +// char_traits // +///////////////// + +// Primary template of char_traits calls std char_traits +template +struct char_traits : std::char_traits +{}; + +// Explicitly define char traits for unsigned char since it is not standard +template<> +struct char_traits : std::char_traits +{ + using char_type = unsigned char; + using int_type = uint64_t; + + // Redefine to_int_type function + static int_type to_int_type(char_type c) noexcept + { + return static_cast(c); + } + + static char_type to_char_type(int_type i) noexcept + { + return static_cast(i); + } + + static constexpr int_type eof() noexcept + { + return static_cast(std::char_traits::eof()); + } +}; + +// Explicitly define char traits for signed char since it is not standard +template<> +struct char_traits : std::char_traits +{ + using char_type = signed char; + using int_type = uint64_t; + + // Redefine to_int_type function + static int_type to_int_type(char_type c) noexcept + { + return static_cast(c); + } + + static char_type to_char_type(int_type i) noexcept + { + return static_cast(i); + } + + static constexpr int_type eof() noexcept + { + return static_cast(std::char_traits::eof()); + } +}; + +/////////////////// +// is_ functions // +/////////////////// + +// https://en.cppreference.com/w/cpp/types/conjunction +template struct conjunction : std::true_type { }; +template struct conjunction : B { }; +template +struct conjunction +: std::conditional(B::value), conjunction, B>::type {}; + +// https://en.cppreference.com/w/cpp/types/negation +template struct negation : std::integral_constant < bool, !B::value > { }; + +// Reimplementation of is_constructible and is_default_constructible, due to them being broken for +// std::pair and std::tuple until LWG 2367 fix (see https://cplusplus.github.io/LWG/lwg-defects.html#2367). +// This causes compile errors in e.g. clang 3.5 or gcc 4.9. +template +struct is_default_constructible : std::is_default_constructible {}; + +template +struct is_default_constructible> + : conjunction, is_default_constructible> {}; + +template +struct is_default_constructible> + : conjunction, is_default_constructible> {}; + +template +struct is_default_constructible> + : conjunction...> {}; + +template +struct is_default_constructible> + : conjunction...> {}; + +template +struct is_constructible : std::is_constructible {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_iterator_traits : std::false_type {}; + +template +struct is_iterator_traits> +{ + private: + using traits = iterator_traits; + + public: + static constexpr auto value = + is_detected::value && + is_detected::value && + is_detected::value && + is_detected::value && + is_detected::value; +}; + +template +struct is_range +{ + private: + using t_ref = typename std::add_lvalue_reference::type; + + using iterator = detected_t; + using sentinel = detected_t; + + // to be 100% correct, it should use https://en.cppreference.com/w/cpp/iterator/input_or_output_iterator + // and https://en.cppreference.com/w/cpp/iterator/sentinel_for + // but reimplementing these would be too much work, as a lot of other concepts are used underneath + static constexpr auto is_iterator_begin = + is_iterator_traits>::value; + + public: + static constexpr bool value = !std::is_same::value && !std::is_same::value && is_iterator_begin; +}; + +template +using iterator_t = enable_if_t::value, result_of_begin())>>; + +template +using range_value_t = value_type_t>>; + +// The following implementation of is_complete_type is taken from +// https://blogs.msdn.microsoft.com/vcblog/2015/12/02/partial-support-for-expression-sfinae-in-vs-2015-update-1/ +// and is written by Xiang Fan who agreed to using it in this library. + +template +struct is_complete_type : std::false_type {}; + +template +struct is_complete_type : std::true_type {}; + +template +struct is_compatible_object_type_impl : std::false_type {}; + +template +struct is_compatible_object_type_impl < + BasicJsonType, CompatibleObjectType, + enable_if_t < is_detected::value&& + is_detected::value >> +{ + using object_t = typename BasicJsonType::object_t; + + // macOS's is_constructible does not play well with nonesuch... + static constexpr bool value = + is_constructible::value && + is_constructible::value; +}; + +template +struct is_compatible_object_type + : is_compatible_object_type_impl {}; + +template +struct is_constructible_object_type_impl : std::false_type {}; + +template +struct is_constructible_object_type_impl < + BasicJsonType, ConstructibleObjectType, + enable_if_t < is_detected::value&& + is_detected::value >> +{ + using object_t = typename BasicJsonType::object_t; + + static constexpr bool value = + (is_default_constructible::value && + (std::is_move_assignable::value || + std::is_copy_assignable::value) && + (is_constructible::value && + std::is_same < + typename object_t::mapped_type, + typename ConstructibleObjectType::mapped_type >::value)) || + (has_from_json::value || + has_non_default_from_json < + BasicJsonType, + typename ConstructibleObjectType::mapped_type >::value); +}; + +template +struct is_constructible_object_type + : is_constructible_object_type_impl {}; + +template +struct is_compatible_string_type +{ + static constexpr auto value = + is_constructible::value; +}; + +template +struct is_constructible_string_type +{ + // launder type through decltype() to fix compilation failure on ICPC +#ifdef __INTEL_COMPILER + using laundered_type = decltype(std::declval()); +#else + using laundered_type = ConstructibleStringType; +#endif + + static constexpr auto value = + conjunction < + is_constructible, + is_detected_exact>::value; +}; + +template +struct is_compatible_array_type_impl : std::false_type {}; + +template +struct is_compatible_array_type_impl < + BasicJsonType, CompatibleArrayType, + enable_if_t < + is_detected::value&& + is_iterator_traits>>::value&& +// special case for types like std::filesystem::path whose iterator's value_type are themselves +// c.f. https://github.com/nlohmann/json/pull/3073 + !std::is_same>::value >> +{ + static constexpr bool value = + is_constructible>::value; +}; + +template +struct is_compatible_array_type + : is_compatible_array_type_impl {}; + +template +struct is_constructible_array_type_impl : std::false_type {}; + +template +struct is_constructible_array_type_impl < + BasicJsonType, ConstructibleArrayType, + enable_if_t::value >> + : std::true_type {}; + +template +struct is_constructible_array_type_impl < + BasicJsonType, ConstructibleArrayType, + enable_if_t < !std::is_same::value&& + !is_compatible_string_type::value&& + is_default_constructible::value&& +(std::is_move_assignable::value || + std::is_copy_assignable::value)&& +is_detected::value&& +is_iterator_traits>>::value&& +is_detected::value&& +// special case for types like std::filesystem::path whose iterator's value_type are themselves +// c.f. https://github.com/nlohmann/json/pull/3073 +!std::is_same>::value&& +is_complete_type < +detected_t>::value >> +{ + using value_type = range_value_t; + + static constexpr bool value = + std::is_same::value || + has_from_json::value || + has_non_default_from_json < + BasicJsonType, + value_type >::value; +}; + +template +struct is_constructible_array_type + : is_constructible_array_type_impl {}; + +template +struct is_compatible_integer_type_impl : std::false_type {}; + +template +struct is_compatible_integer_type_impl < + RealIntegerType, CompatibleNumberIntegerType, + enable_if_t < std::is_integral::value&& + std::is_integral::value&& + !std::is_same::value >> +{ + // is there an assert somewhere on overflows? + using RealLimits = std::numeric_limits; + using CompatibleLimits = std::numeric_limits; + + static constexpr auto value = + is_constructible::value && + CompatibleLimits::is_integer && + RealLimits::is_signed == CompatibleLimits::is_signed; +}; + +template +struct is_compatible_integer_type + : is_compatible_integer_type_impl {}; + +template +struct is_compatible_type_impl: std::false_type {}; + +template +struct is_compatible_type_impl < + BasicJsonType, CompatibleType, + enable_if_t::value >> +{ + static constexpr bool value = + has_to_json::value; +}; + +template +struct is_compatible_type + : is_compatible_type_impl {}; + +template +struct is_constructible_tuple : std::false_type {}; + +template +struct is_constructible_tuple> : conjunction...> {}; + +template +struct is_json_iterator_of : std::false_type {}; + +template +struct is_json_iterator_of : std::true_type {}; + +template +struct is_json_iterator_of : std::true_type +{}; + +// checks if a given type T is a template specialization of Primary +template