From 296c7684852040b855c11b93b6d98724c7371866 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Thu, 5 Feb 2026 02:13:11 +0100 Subject: [PATCH 01/17] feat(network): Send product information to distinguish clients. --- .../GameEngine/Include/GameNetwork/GameInfo.h | 24 ++ Core/GameEngine/Include/GameNetwork/LANAPI.h | 43 ++++ .../Include/GameNetwork/LANPlayer.h | 5 +- .../Source/GameNetwork/GameInfo.cpp | 7 + Core/GameEngine/Source/GameNetwork/LANAPI.cpp | 23 ++ .../Source/GameNetwork/LANAPIhandlers.cpp | 207 ++++++++++++++++++ .../GameEngine/Include/Common/GameEngine.h | 4 + .../GameEngine/Include/GameLogic/GameLogic.h | 2 + .../GameEngine/Source/Common/GameEngine.cpp | 7 + .../Source/GameLogic/System/GameLogic.cpp | 3 + 10 files changed, 324 insertions(+), 1 deletion(-) diff --git a/Core/GameEngine/Include/GameNetwork/GameInfo.h b/Core/GameEngine/Include/GameNetwork/GameInfo.h index b51b6ae7ff0..a5342e9a1a6 100644 --- a/Core/GameEngine/Include/GameNetwork/GameInfo.h +++ b/Core/GameEngine/Include/GameNetwork/GameInfo.h @@ -57,6 +57,26 @@ enum class GameSlot { public: + struct ProductInfo + { + enum CPP_11(: UnsignedInt) + { + NO_RETAIL = 1 << 0, + SHELLMAP_ENABLED = 1 << 1, + ZERO_MAPS_STARTED = 1 << 2 + }; + + UnsignedInt flags; + UnsignedInt uptime; + UnsignedInt exeCRC; + UnsignedInt iniCRC; + UnsignedInt fpMathCRC; + UnicodeString productTitle; + UnicodeString productVersion; + UnicodeString productAuthor; + UnicodeString gitShortHash; + }; + GameSlot(); virtual void reset(); @@ -125,6 +145,9 @@ class GameSlot void mute( Bool isMuted ) { m_isMuted = isMuted; } Bool isMuted( void ) const { return m_isMuted; } + + void setProductInfo(const ProductInfo& productInfo) { m_productInfo = productInfo; } + const ProductInfo& getProductInfo() const { return m_productInfo; } protected: SlotState m_state; Bool m_isAccepted; @@ -143,6 +166,7 @@ class GameSlot FirewallHelperClass::FirewallBehaviorType m_NATBehavior; ///< The NAT behavior for this slot's player. UnsignedInt m_lastFrameInGame; // only valid for human players Bool m_disconnected; // only valid for human players + ProductInfo m_productInfo; ///< Community made product information }; /** diff --git a/Core/GameEngine/Include/GameNetwork/LANAPI.h b/Core/GameEngine/Include/GameNetwork/LANAPI.h index 5c737ad8e1c..b2a416bdfbd 100644 --- a/Core/GameEngine/Include/GameNetwork/LANAPI.h +++ b/Core/GameEngine/Include/GameNetwork/LANAPI.h @@ -173,6 +173,25 @@ struct LANMessage MSG_INACTIVE, ///< I've alt-tabbed out. Unaccept me cause I'm a poo-flinging monkey. MSG_REQUEST_GAME_INFO, ///< For direct connect, get the game info from a specific IP Address + + // TheSuperHackers @feature Caball009 05/02/2026 Product information is exchanged on demand and never broadcast. + // A client is considered 'patched' if it responds to a product info request (or, in pre-match, if it sends one). + // The implementation consists of three parts. + // 1. player - player in lobby: + // - When a player detects a new player in the lobby, it sends a product info request. + // - If the other player responds with an acknowledgement, they are considered patched. + // 2. player - host in lobby: + // - When a player detects a new game host in the lobby, it sends a product info request. + // - If the host responds with an acknowledgement, it is considered patched. + // 3. players in pre-match: + // - When a player joins a match, it sends a product info request to all existing players. + // - Existing players treat this request as confirmation that the joining player is patched (no explicit acknowledgement required). + MSG_GAME_REQUEST_PRODUCT_INFO = 1000, + MSG_GAME_RESPONSE_PRODUCT_INFO, + MSG_LOBBY_REQUEST_PRODUCT_INFO, + MSG_LOBBY_RESPONSE_PRODUCT_INFO, + MSG_MATCH_REQUEST_PRODUCT_INFO, + MSG_MATCH_RESPONSE_PRODUCT_INFO, } messageType; WideChar name[g_lanPlayerNameLength+1]; ///< My name, for convenience @@ -267,6 +286,17 @@ struct LANMessage char options[m_lanMaxOptionsLength+1]; } GameOptions; + // ProductInfo is sent with REQUEST_PRODUCT_INFO and RESPONSE_PRODUCT_INFO + struct + { + UnsignedInt flags; + UnsignedInt uptime; + UnsignedInt exeCRC; + UnsignedInt iniCRC; + UnsignedInt fpMathCRC; + WideChar data[201]; + Byte padding[20]; + } ProductInfo; }; }; #pragma pack(pop) @@ -394,6 +424,12 @@ class LANAPI : public LANAPIInterface void addGame(LANGameInfo *game); AsciiString createSlotString( void ); + static UnsignedInt getProductInfoFlags(); + static void setProductInfoFromLocalData(GameSlot *slot); + static void setProductInfoFromMessage(LANMessage *msg, GameSlot *slot); + static Bool setProductInfoStrings(const UnicodeString(&input)[4], WideChar(&output)[201]); + static Bool getProductInfoStrings(WideChar(&input)[201], UnicodeString*(&output)[4]); + // Functions to handle incoming messages ----------------------------------- void handleRequestLocations( LANMessage *msg, UnsignedInt senderIP ); void handleGameAnnounce( LANMessage *msg, UnsignedInt senderIP ); @@ -412,4 +448,11 @@ class LANAPI : public LANAPIInterface void handleGameOptions( LANMessage *msg, UnsignedInt senderIP ); void handleInActive( LANMessage *msg, UnsignedInt senderIP ); + void sendProductInfoMessage(LANMessage::Type messageType, UnsignedInt senderIP); + void handleGameProductInfoRequest(LANMessage *msg, UnsignedInt senderIP); + void handleGameProductInfoResponse(LANMessage *msg, UnsignedInt senderIP); + void handleLobbyProductInfoRequest(LANMessage *msg, UnsignedInt senderIP); + void handleLobbyProductInfoResponse(LANMessage *msg, UnsignedInt senderIP); + void handleMatchProductInfoRequest(LANMessage *msg, UnsignedInt senderIP); + void handleMatchProductInfoResponse(LANMessage *msg, UnsignedInt senderIP); }; diff --git a/Core/GameEngine/Include/GameNetwork/LANPlayer.h b/Core/GameEngine/Include/GameNetwork/LANPlayer.h index e7c4e2f1e16..90ef0553919 100644 --- a/Core/GameEngine/Include/GameNetwork/LANPlayer.h +++ b/Core/GameEngine/Include/GameNetwork/LANPlayer.h @@ -35,7 +35,7 @@ class LANPlayer { public: - LANPlayer() { m_name = m_login = m_host = L""; m_lastHeard = 0; m_next = nullptr; m_IP = 0; } + LANPlayer() { m_name = m_login = m_host = L""; m_lastHeard = 0; m_next = nullptr; m_IP = 0; m_productInfoFlags = 0; } // Access functions UnicodeString getName( void ) { return m_name; } @@ -52,6 +52,8 @@ class LANPlayer void setNext( LANPlayer *next ) { m_next = next; } UnsignedInt getIP( void ) { return m_IP; } void setIP( UnsignedInt IP ) { m_IP = IP; } + void setProductInfoFlags(UnsignedInt productInfoFlags) { m_productInfoFlags = productInfoFlags; } + UnsignedInt getProductInfoFlags() const { return m_productInfoFlags; } protected: UnicodeString m_name; ///< Player name @@ -60,4 +62,5 @@ class LANPlayer UnsignedInt m_lastHeard; ///< The last time we heard from this player (for timeout purposes) LANPlayer *m_next; ///< Linked list pointer UnsignedInt m_IP; ///< Player's IP + UnsignedInt m_productInfoFlags; ///< Community made product information flags }; diff --git a/Core/GameEngine/Source/GameNetwork/GameInfo.cpp b/Core/GameEngine/Source/GameNetwork/GameInfo.cpp index 0b5370f0947..9244d2a0f23 100644 --- a/Core/GameEngine/Source/GameNetwork/GameInfo.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameInfo.cpp @@ -73,6 +73,7 @@ void GameSlot::reset() m_origPlayerTemplate = -1; m_origStartPos = -1; m_origColor = -1; + m_productInfo = ProductInfo(); } void GameSlot::saveOffOriginalInfo( void ) @@ -1493,7 +1494,13 @@ Bool ParseAsciiStringToGameInfo(GameInfo *game, AsciiString options) //DEBUG_LOG(("ParseAsciiStringToGameInfo - game options all good, setting info")); for(Int i = 0; igetConstSlot(i)->getState() == SLOT_PLAYER && newSlot[i].getState() == SLOT_PLAYER) + newSlot[i].setProductInfo(game->getConstSlot(i)->getProductInfo()); + game->setSlot(i,newSlot[i]); + } game->setMap(mapName); game->setMapCRC(mapCRC); diff --git a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp index 03a99fa76a5..150a15e4297 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp @@ -425,6 +425,26 @@ void LANAPI::update( void ) handleInActive( msg, senderIP ); break; + // exchange product information with other players + case LANMessage::MSG_GAME_REQUEST_PRODUCT_INFO: + handleGameProductInfoRequest(msg, senderIP); + break; + case LANMessage::MSG_GAME_RESPONSE_PRODUCT_INFO: + handleGameProductInfoResponse(msg, senderIP); + break; + case LANMessage::MSG_LOBBY_REQUEST_PRODUCT_INFO: + handleLobbyProductInfoRequest(msg, senderIP); + break; + case LANMessage::MSG_LOBBY_RESPONSE_PRODUCT_INFO: + handleLobbyProductInfoResponse(msg, senderIP); + break; + case LANMessage::MSG_MATCH_REQUEST_PRODUCT_INFO: + handleMatchProductInfoRequest(msg, senderIP); + break; + case LANMessage::MSG_MATCH_RESPONSE_PRODUCT_INFO: + handleMatchProductInfoResponse(msg, senderIP); + break; + default: DEBUG_LOG(("Unknown LAN message type %d", msg->messageType)); } @@ -906,6 +926,9 @@ void LANAPI::RequestGameCreate( UnicodeString gameName, Bool isDirectConnect ) newSlot.setLogin(m_userName); newSlot.setHost(m_hostName); + // set product information for local game slot + setProductInfoFromLocalData(&newSlot); + myGame->setSlot(0,newSlot); myGame->setNext(nullptr); LANPreferences pref; diff --git a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index 0f44f1d681d..f22e2bf249d 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -36,9 +36,203 @@ #include "Common/GlobalData.h" #include "Common/QuotedPrintable.h" #include "Common/UserPreferences.h" +#include "Common/GameEngine.h" +#include "Common/version.h" #include "GameNetwork/LANAPI.h" #include "GameNetwork/LANAPICallbacks.h" #include "GameClient/MapUtil.h" +#include "GameLogic/GameLogic.h" + +Bool LANAPI::setProductInfoStrings(const UnicodeString(&input)[4], WideChar(&output)[201]) +{ + // concatenate strings separated by null terminators + + // null terminate the output buffer to mirror the get function + output[ARRAY_SIZE(output) - 1] = 0; + + size_t totalSize = 0; + for (size_t i = 0; i < ARRAY_SIZE(input); ++i) + { + totalSize += wcslcpy(output + totalSize, input[i].str(), ARRAY_SIZE(output) - totalSize) + 1; + if (totalSize >= ARRAY_SIZE(output)) + { + return FALSE; + } + } + + return totalSize <= ARRAY_SIZE(output); +} + +Bool LANAPI::getProductInfoStrings(WideChar(&input)[201], UnicodeString*(&output)[4]) +{ + // extract strings separated by null terminators + + // null terminate the input buffer to prevent potential out-of-bound reads + const Bool nullTerminated = !static_cast(input[ARRAY_SIZE(input) - 1]); + input[ARRAY_SIZE(input) - 1] = 0; + + for (size_t totalSize = 0, i = 0; i < ARRAY_SIZE(output); ++i) + { + const size_t size = wcslen(input + totalSize); + output[i]->set(input + totalSize, size); + + totalSize += size + 1; + if (!nullTerminated && totalSize >= ARRAY_SIZE(input)) + { + for (size_t j = i + 1; j < ARRAY_SIZE(output); ++j) + { + output[j]->clear(); + } + + return FALSE; + } + } + + return TRUE; +} + +UnsignedInt LANAPI::getProductInfoFlags() +{ + const UnsignedInt flags = GameSlot::ProductInfo::NO_RETAIL + | (GameSlot::ProductInfo::SHELLMAP_ENABLED * TheGlobalData->m_shellMapOn) + | (GameSlot::ProductInfo::ZERO_MAPS_STARTED * (TheGameLogic->getStartedGamesCount() == 0)); + + return flags; +} + +void LANAPI::setProductInfoFromLocalData(GameSlot *slot) +{ + GameSlot::ProductInfo productInfo; + productInfo.flags = getProductInfoFlags(); + productInfo.uptime = TheGameEngine->getUptime(); + productInfo.exeCRC = TheGlobalData->m_exeCRC; + productInfo.iniCRC = TheGlobalData->m_iniCRC; + productInfo.fpMathCRC = 0; // needs to be replaced with SimulationMathCrc::calculate() from #2100 + productInfo.productTitle = TheVersion->getUnicodeProductTitle(); + productInfo.productVersion = TheVersion->getUnicodeProductVersion(); + productInfo.productAuthor = TheVersion->getUnicodeProductAuthor(); + productInfo.gitShortHash = TheVersion->getUnicodeGitShortHash(); + + slot->setProductInfo(productInfo); +} + +void LANAPI::setProductInfoFromMessage(LANMessage *msg, GameSlot *slot) +{ + GameSlot::ProductInfo productInfo; + productInfo.flags = msg->ProductInfo.flags; + productInfo.uptime = msg->ProductInfo.uptime; + productInfo.exeCRC = msg->ProductInfo.exeCRC; + productInfo.iniCRC = msg->ProductInfo.iniCRC; + productInfo.fpMathCRC = msg->ProductInfo.fpMathCRC; + + UnicodeString *strings[] = + { + &productInfo.productTitle, + &productInfo.productVersion, + &productInfo.productAuthor, + &productInfo.gitShortHash + }; + getProductInfoStrings(msg->ProductInfo.data, strings); + + slot->setProductInfo(productInfo); +} + +void LANAPI::sendProductInfoMessage(LANMessage::Type messageType, UnsignedInt senderIP) +{ + LANMessage msg; + fillInLANMessage(&msg); + msg.messageType = messageType; + + msg.ProductInfo.flags = getProductInfoFlags(); + msg.ProductInfo.uptime = TheGameEngine->getUptime(); + msg.ProductInfo.exeCRC = TheGlobalData->m_exeCRC; + msg.ProductInfo.iniCRC = TheGlobalData->m_iniCRC; + msg.ProductInfo.fpMathCRC = 0; // needs to be replaced with SimulationMathCrc::calculate() from #2100 + + const UnicodeString strings[] = + { + TheVersion->getUnicodeProductTitle(), + TheVersion->getUnicodeProductVersion(), + TheVersion->getUnicodeProductAuthor(), + TheVersion->getUnicodeGitShortHash() + }; + setProductInfoStrings(strings, msg.ProductInfo.data); + + sendMessage(&msg, senderIP); +} + +void LANAPI::handleGameProductInfoRequest(LANMessage *msg, UnsignedInt senderIP) +{ + if (!AmIHost()) + return; + + // acknowledge as game host a request for product information by a player in the lobby + sendProductInfoMessage(LANMessage::MSG_GAME_RESPONSE_PRODUCT_INFO, senderIP); +} + +void LANAPI::handleGameProductInfoResponse(LANMessage *msg, UnsignedInt senderIP) +{ + if (!m_inLobby) + return; + + LANGameInfo *game = LookupGameByHost(senderIP); + if (!game) + return; + + // a game host has acknowledged our request for product information + setProductInfoFromMessage(msg, game->getSlot(0)); +} + +void LANAPI::handleLobbyProductInfoRequest(LANMessage *msg, UnsignedInt senderIP) +{ + if (!m_inLobby) + return; + + // acknowledge a request for product information by a fellow player in the lobby + sendProductInfoMessage(LANMessage::MSG_LOBBY_RESPONSE_PRODUCT_INFO, senderIP); +} + +void LANAPI::handleLobbyProductInfoResponse(LANMessage *msg, UnsignedInt senderIP) +{ + if (!m_inLobby) + return; + + LANPlayer *player = LookupPlayer(senderIP); + if (!player) + return; + + // a fellow player in the lobby has acknowledged our request for product information + player->setProductInfoFlags(msg->ProductInfo.flags); +} + +void LANAPI::handleMatchProductInfoRequest(LANMessage *msg, UnsignedInt senderIP) +{ + if (!m_currentGame) + return; + + // acknowledge a request for product information by a fellow player in the game + sendProductInfoMessage(LANMessage::MSG_MATCH_RESPONSE_PRODUCT_INFO, senderIP); + + // treat request for product information as acknowledgement + handleMatchProductInfoResponse(msg, senderIP); +} + +void LANAPI::handleMatchProductInfoResponse(LANMessage *msg, UnsignedInt senderIP) +{ + if (!m_currentGame) + return; + + for (Int i = 0; i < MAX_SLOTS; ++i) + { + if (!m_currentGame->getConstSlot(i)->isHuman() || m_currentGame->getIP(i) != senderIP) + continue; + + // a fellow player in the game has acknowledged our request for product information + setProductInfoFromMessage(msg, m_currentGame->getSlot(i)); + + break; + } +} void LANAPI::handleRequestLocations( LANMessage *msg, UnsignedInt senderIP ) { @@ -140,6 +334,9 @@ void LANAPI::handleGameAnnounce( LANMessage *msg, UnsignedInt senderIP ) game = NEW LANGameInfo; game->setName(UnicodeString(msg->GameInfo.gameName)); addGame(game); + + // request a game host to send product information + sendProductInfoMessage(LANMessage::MSG_GAME_REQUEST_PRODUCT_INFO, senderIP); } Bool success = ParseGameOptionsString(game,AsciiString(msg->GameInfo.options)); game->setGameInProgress(msg->GameInfo.inProgress); @@ -166,6 +363,9 @@ void LANAPI::handleLobbyAnnounce( LANMessage *msg, UnsignedInt senderIP ) { player = NEW LANPlayer; player->setIP(senderIP); + + // request a player in the lobby to send product information + sendProductInfoMessage(LANMessage::MSG_LOBBY_REQUEST_PRODUCT_INFO, senderIP); } else { @@ -451,6 +651,10 @@ void LANAPI::handleJoinAccept( LANMessage *msg, UnsignedInt senderIP ) slot.setLastHeard(0); slot.setLogin(m_userName); slot.setHost(m_hostName); + + // set product information for local game slot + setProductInfoFromLocalData(&slot); + m_currentGame->setSlot(pos, slot); m_currentGame->getLANSlot(0)->setHost(msg->hostName); @@ -465,6 +669,9 @@ void LANAPI::handleJoinAccept( LANMessage *msg, UnsignedInt senderIP ) OnGameJoin(RET_OK, m_currentGame); //DEBUG_ASSERTCRASH(false, ("setting host to %ls@%ls", m_currentGame->getLANSlot(0)->getUser()->getLogin().str(), // m_currentGame->getLANSlot(0)->getUser()->getHost().str())); + + // request players in the match to send product information + sendProductInfoMessage(LANMessage::MSG_MATCH_REQUEST_PRODUCT_INFO, 0); } m_pendingAction = ACT_NONE; m_expiration = 0; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h index 62d3502eed1..b7f5ea1f9a3 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h @@ -75,6 +75,8 @@ class GameEngine : public SubsystemInterface virtual Bool isActive(void) {return m_isActive;} ///< returns whether app has OS focus. virtual void setIsActive(Bool isActive) { m_isActive = isActive; }; + UnsignedInt getUptime() const; ///< returns time since engine creation + protected: virtual void resetSubsystems( void ); @@ -101,6 +103,8 @@ class GameEngine : public SubsystemInterface Bool m_quitting; ///< true when we need to quit the game Bool m_isActive; ///< app has OS focus. + + UnsignedInt m_launchTime; ///< stores the system time when the game engine was created }; inline void GameEngine::setQuitting( Bool quitting ) { m_quitting = quitting; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h index 48522894f90..2ae4a8af702 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h @@ -137,6 +137,7 @@ class GameLogic : public SubsystemInterface, public Snapshot Bool hasUpdated() const { return m_hasUpdated; } ///< Returns true if the logic frame has advanced in the current client/render update UnsignedInt getFrame( void ); ///< Returns the current simulation frame number UnsignedInt getCRC( Int mode = CRC_CACHED, AsciiString deepCRCFileName = AsciiString::TheEmptyString ); ///< Returns the CRC + UnsignedInt getStartedGamesCount() const { return m_startedGamesCount; } ///< Returns the total number of map starts since game launch void setObjectIDCounter( ObjectID nextObjID ) { m_nextObjID = nextObjID; } ObjectID getObjectIDCounter( void ) { return m_nextObjID; } @@ -389,6 +390,7 @@ class GameLogic : public SubsystemInterface, public Snapshot #endif UnsignedInt m_frameObjectsChangedTriggerAreas; ///< Last frame objects moved into/outof trigger areas, or were created/destroyed. jba. + UnsignedInt m_startedGamesCount; ///< total number of map starts since game launch, excluding the shell map // ---------------------------------------------------------------------------------------------- struct ObjectTOCEntry diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index 5aa3a1e7b4d..7dc8b47e90a 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -251,6 +251,7 @@ GameEngine::GameEngine( void ) m_logicTimeAccumulator = 0.0f; m_quitting = FALSE; m_isActive = FALSE; + m_launchTime = ::timeGetTime(); _Module.Init(nullptr, ApplicationHInstance, nullptr); } @@ -1033,6 +1034,12 @@ Bool GameEngine::isMultiplayerSession( void ) return TheRecorder->isMultiplayer(); } +//------------------------------------------------------------------------------------------------- +UnsignedInt GameEngine::getUptime() const +{ + return ::timeGetTime() - m_launchTime; +} + //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 51771f62eba..1383233e302 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -262,6 +262,8 @@ GameLogic::GameLogic( void ) m_loadingMap = FALSE; m_loadingSave = FALSE; m_clearingGameData = FALSE; + + m_startedGamesCount = 0; } //------------------------------------------------------------------------------------------------- @@ -1113,6 +1115,7 @@ void GameLogic::startNewGame( Bool loadingSaveGame ) // reset the frame counter m_frame = 0; m_hasUpdated = FALSE; + m_startedGamesCount += (m_gameMode != GAME_SHELL); #ifdef DEBUG_CRC // TheSuperHackers @info helmutbuhler 04/09/2025 From 7a7775e921c8fe4bbac1750162b0c238a6d21dfb Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Thu, 5 Feb 2026 02:14:07 +0100 Subject: [PATCH 02/17] Added color and status code. --- .../Include/GameNetwork/LANAPICallbacks.h | 6 ++++ .../Source/GameNetwork/LANAPICallbacks.cpp | 31 +++++++++++------ .../Source/GameNetwork/LANAPIhandlers.cpp | 9 +++++ .../Source/GameNetwork/LANGameInfo.cpp | 7 +++- .../GameClient/GUI/GUICallbacks/Diplomacy.cpp | 34 +++++++++++++------ .../GameEngine/Source/GameNetwork/GUIUtil.cpp | 11 ++++++ 6 files changed, 76 insertions(+), 22 deletions(-) diff --git a/Core/GameEngine/Include/GameNetwork/LANAPICallbacks.h b/Core/GameEngine/Include/GameNetwork/LANAPICallbacks.h index 394b75dedd8..5c7cc993320 100644 --- a/Core/GameEngine/Include/GameNetwork/LANAPICallbacks.h +++ b/Core/GameEngine/Include/GameNetwork/LANAPICallbacks.h @@ -68,6 +68,12 @@ extern const Color chatSystemColor; extern const Color chatSystemColor; extern const Color acceptTrueColor; extern const Color acceptFalseColor; +extern const Color gameColorCommunityPatch; +extern const Color gameInProgressColorCommunityPatch; +extern const Color playerColorCommunityPatch; +extern const Color playerGrayedColorCommunityPatch; +extern const Color playerColorCommunityPatchIssue; +extern const Color playerGrayedColorCommunityPatchIssue; void lanUpdateSlotList( void ); diff --git a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp index 2c344553f76..6f588377922 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp @@ -52,16 +52,22 @@ extern Bool LANbuttonPushed; //Colors used for the chat dialogs -const Color playerColor = GameMakeColor(255,255,255,255); -const Color gameColor = GameMakeColor(255,255,255,255); -const Color gameInProgressColor = GameMakeColor(128,128,128,255); -const Color chatNormalColor = GameMakeColor(50,215,230,255); -const Color chatActionColor = GameMakeColor(255,0,255,255); -const Color chatLocalNormalColor = GameMakeColor(255,128,0,255); -const Color chatLocalActionColor = GameMakeColor(128,255,255,255); -const Color chatSystemColor = GameMakeColor(255,255,255,255); -const Color acceptTrueColor = GameMakeColor(0,255,0,255); -const Color acceptFalseColor = GameMakeColor(255,0,0,255); +const Color playerColor = GameMakeColor(255,255,255,255); +const Color gameColor = GameMakeColor(255,255,255,255); +const Color gameInProgressColor = GameMakeColor(128,128,0,255); +const Color chatNormalColor = GameMakeColor(50,215,230,255); +const Color chatActionColor = GameMakeColor(255,0,255,255); +const Color chatLocalNormalColor = GameMakeColor(255,128,0,255); +const Color chatLocalActionColor = GameMakeColor(128,255,255,255); +const Color chatSystemColor = GameMakeColor(255,255,255,255); +const Color acceptTrueColor = GameMakeColor(0,255,0,255); +const Color acceptFalseColor = GameMakeColor(255,0,0,255); +const Color gameColorCommunityPatch = GameMakeColor(255,255,0,255); +const Color gameInProgressColorCommunityPatch = GameMakeColor(192,192,0,255); +const Color playerColorCommunityPatch = gameColorCommunityPatch; +const Color playerGrayedColorCommunityPatch = gameInProgressColorCommunityPatch; +const Color playerColorCommunityPatchIssue = GameMakeColor(255,0,0,255); +const Color playerGrayedColorCommunityPatchIssue = GameMakeColor(192,0,0,255); UnicodeString LANAPIInterface::getErrorStringFromReturnType( ReturnType ret ) @@ -640,7 +646,10 @@ void LANAPI::OnPlayerList( LANPlayer *playerList ) LANPlayer *player = m_lobbyPlayers; while (player) { - Int addedIndex = GadgetListBoxAddEntryText(listboxPlayers, player->getName(), playerColor, -1, -1); + // TheSuperHackers @feature Caball009 06/11/2025 Set special color for players that are using the community patch. + const Color color = (m_localIP == player->getIP() || BitIsSet(player->getProductInfoFlags(), GameSlot::ProductInfo::NO_RETAIL)) + ? playerColorCommunityPatch : playerColor; + const Int addedIndex = GadgetListBoxAddEntryText(listboxPlayers, player->getName(), color, -1, -1); GadgetListBoxSetItemData(listboxPlayers, (void *)player->getIP(),addedIndex, 0 ); if (selectedIP == player->getIP()) diff --git a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index f22e2bf249d..5b3a1472e50 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -181,6 +181,9 @@ void LANAPI::handleGameProductInfoResponse(LANMessage *msg, UnsignedInt senderIP // a game host has acknowledged our request for product information setProductInfoFromMessage(msg, game->getSlot(0)); + + // update game list with colored names + OnGameList(m_games); } void LANAPI::handleLobbyProductInfoRequest(LANMessage *msg, UnsignedInt senderIP) @@ -203,6 +206,9 @@ void LANAPI::handleLobbyProductInfoResponse(LANMessage *msg, UnsignedInt senderI // a fellow player in the lobby has acknowledged our request for product information player->setProductInfoFlags(msg->ProductInfo.flags); + + // update player list with colored names + OnPlayerList(m_lobbyPlayers); } void LANAPI::handleMatchProductInfoRequest(LANMessage *msg, UnsignedInt senderIP) @@ -230,6 +236,9 @@ void LANAPI::handleMatchProductInfoResponse(LANMessage *msg, UnsignedInt senderI // a fellow player in the game has acknowledged our request for product information setProductInfoFromMessage(msg, m_currentGame->getSlot(i)); + // update player list with colored names + lanUpdateSlotList(); + break; } } diff --git a/Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp b/Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp index 638681e4420..c55a1b6cf3f 100644 --- a/Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp @@ -224,7 +224,12 @@ void LANDisplayGameList( GameWindow *gameListbox, LANGameInfo *gameList ) { txtGName.concat(L"]"); } - Int addedIndex = GadgetListBoxAddEntryText(gameListbox, txtGName, (gameList->isGameInProgress())?gameInProgressColor:gameColor, -1, -1); + + // TheSuperHackers @feature Caball009 06/11/2025 Set special color for games that are using the community patch. + const Color color = (BitIsSet(gameList->getSlot(0)->getProductInfo().flags, GameSlot::ProductInfo::NO_RETAIL)) + ? ((gameList->isGameInProgress()) ? gameInProgressColorCommunityPatch : gameColorCommunityPatch) + : ((gameList->isGameInProgress()) ? gameInProgressColor : gameColor); + const Int addedIndex = GadgetListBoxAddEntryText(gameListbox, txtGName, color, -1, -1); GadgetListBoxSetItemData(gameListbox, (void *)gameList, addedIndex, 0 ); if (selectedPtr == gameList) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Diplomacy.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Diplomacy.cpp index 115a315e462..f4ce812ad90 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Diplomacy.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Diplomacy.cpp @@ -36,6 +36,7 @@ #include "Common/PlayerList.h" #include "Common/PlayerTemplate.h" #include "Common/Recorder.h" +#include "Common/version.h" #include "GameClient/AnimateWindowManager.h" #include "GameClient/Diplomacy.h" #include "GameClient/DisconnectMenu.h" @@ -532,24 +533,28 @@ void PopulateInGameDiplomacyPopup( void ) if (staticTextStatus[rowNum]) { staticTextStatus[rowNum]->winHide(FALSE); + + Color frontColor = 0; + UnicodeString text; + if (isInGame) { if (isAlive) { - staticTextStatus[rowNum]->winSetEnabledTextColors( aliveColor, backColor ); - GadgetStaticTextSetText(staticTextStatus[rowNum], TheGameText->fetch("GUI:PlayerAlive")); + frontColor = aliveColor; + text = TheGameText->fetch("GUI:PlayerAlive"); } else { if (isObserver) { - staticTextStatus[rowNum]->winSetEnabledTextColors( observerInGameColor, backColor ); - GadgetStaticTextSetText(staticTextStatus[rowNum], TheGameText->fetch("GUI:PlayerObserver")); + frontColor = observerInGameColor; + text = TheGameText->fetch("GUI:PlayerObserver"); } else { - staticTextStatus[rowNum]->winSetEnabledTextColors( deadColor, backColor ); - GadgetStaticTextSetText(staticTextStatus[rowNum], TheGameText->fetch("GUI:PlayerDead")); + frontColor = deadColor; + text = TheGameText->fetch("GUI:PlayerDead"); } } } @@ -558,15 +563,24 @@ void PopulateInGameDiplomacyPopup( void ) // not in game if (isObserver) { - staticTextStatus[rowNum]->winSetEnabledTextColors( observerGoneColor, backColor ); - GadgetStaticTextSetText(staticTextStatus[rowNum], TheGameText->fetch("GUI:PlayerObserverGone")); + frontColor = observerGoneColor; + text = TheGameText->fetch("GUI:PlayerObserverGone"); } else { - staticTextStatus[rowNum]->winSetEnabledTextColors( goneColor, backColor ); - GadgetStaticTextSetText(staticTextStatus[rowNum], TheGameText->fetch("GUI:PlayerGone")); + frontColor = goneColor; + text = TheGameText->fetch("GUI:PlayerGone"); } } + + // TheSuperHackers @feature Caball009 06/11/2025 Set special status for players that are using the community patch. + if (slot->isHuman() && BitIsSet(slot->getProductInfo().flags, GameSlot::ProductInfo::NO_RETAIL)) + { + text.format(L"%s [%s]", text.str(), slot->getProductInfo().gitShortHash.str()); + } + + staticTextStatus[rowNum]->winSetEnabledTextColors(frontColor, backColor); + GadgetStaticTextSetText(staticTextStatus[rowNum], text); } slotNumInRow[rowNum++] = slotNum; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp index f5ef398b0f8..645cd220059 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp @@ -423,6 +423,17 @@ void UpdateSlotList( GameInfo *myGame, GameWindow *comboPlayer[], } if(slot->isHuman()) { + // TheSuperHackers @feature Caball009 06/11/2025 Set special color for players that are using the community patch. + if (BitIsSet(myGame->getSlot(i)->getProductInfo().flags, GameSlot::ProductInfo::NO_RETAIL)) + { + const Bool setRegularColor = BitIsSet(myGame->getSlot(i)->getProductInfo().flags, GameSlot::ProductInfo::ZERO_MAPS_STARTED); + const Color enabledColor = (setRegularColor) ? playerColorCommunityPatch : playerColorCommunityPatchIssue; + const Color disabledColor = (setRegularColor) ? playerGrayedColorCommunityPatch : playerGrayedColorCommunityPatchIssue; + + GadgetComboBoxSetEnabledTextColors(comboPlayer[i], enabledColor, GameMakeColor(0, 0, 0, 255)); + GadgetComboBoxSetDisabledTextColors(comboPlayer[i], disabledColor, GameMakeColor(0, 0, 0, 255)); + } + UnicodeString newName = slot->getName(); UnicodeString oldName = GadgetComboBoxGetText(comboPlayer[i]); if (comboPlayer[i] && newName.compare(oldName)) From f2651b915042bb61f13c13b2fd409115269494a8 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Thu, 5 Feb 2026 02:14:12 +0100 Subject: [PATCH 03/17] Revert "Added color and status code." This reverts commit 7a7775e921c8fe4bbac1750162b0c238a6d21dfb. --- .../Include/GameNetwork/LANAPICallbacks.h | 6 ---- .../Source/GameNetwork/LANAPICallbacks.cpp | 31 ++++++----------- .../Source/GameNetwork/LANAPIhandlers.cpp | 9 ----- .../Source/GameNetwork/LANGameInfo.cpp | 7 +--- .../GameClient/GUI/GUICallbacks/Diplomacy.cpp | 34 ++++++------------- .../GameEngine/Source/GameNetwork/GUIUtil.cpp | 11 ------ 6 files changed, 22 insertions(+), 76 deletions(-) diff --git a/Core/GameEngine/Include/GameNetwork/LANAPICallbacks.h b/Core/GameEngine/Include/GameNetwork/LANAPICallbacks.h index 5c7cc993320..394b75dedd8 100644 --- a/Core/GameEngine/Include/GameNetwork/LANAPICallbacks.h +++ b/Core/GameEngine/Include/GameNetwork/LANAPICallbacks.h @@ -68,12 +68,6 @@ extern const Color chatSystemColor; extern const Color chatSystemColor; extern const Color acceptTrueColor; extern const Color acceptFalseColor; -extern const Color gameColorCommunityPatch; -extern const Color gameInProgressColorCommunityPatch; -extern const Color playerColorCommunityPatch; -extern const Color playerGrayedColorCommunityPatch; -extern const Color playerColorCommunityPatchIssue; -extern const Color playerGrayedColorCommunityPatchIssue; void lanUpdateSlotList( void ); diff --git a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp index 6f588377922..2c344553f76 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp @@ -52,22 +52,16 @@ extern Bool LANbuttonPushed; //Colors used for the chat dialogs -const Color playerColor = GameMakeColor(255,255,255,255); -const Color gameColor = GameMakeColor(255,255,255,255); -const Color gameInProgressColor = GameMakeColor(128,128,0,255); -const Color chatNormalColor = GameMakeColor(50,215,230,255); -const Color chatActionColor = GameMakeColor(255,0,255,255); -const Color chatLocalNormalColor = GameMakeColor(255,128,0,255); -const Color chatLocalActionColor = GameMakeColor(128,255,255,255); -const Color chatSystemColor = GameMakeColor(255,255,255,255); -const Color acceptTrueColor = GameMakeColor(0,255,0,255); -const Color acceptFalseColor = GameMakeColor(255,0,0,255); -const Color gameColorCommunityPatch = GameMakeColor(255,255,0,255); -const Color gameInProgressColorCommunityPatch = GameMakeColor(192,192,0,255); -const Color playerColorCommunityPatch = gameColorCommunityPatch; -const Color playerGrayedColorCommunityPatch = gameInProgressColorCommunityPatch; -const Color playerColorCommunityPatchIssue = GameMakeColor(255,0,0,255); -const Color playerGrayedColorCommunityPatchIssue = GameMakeColor(192,0,0,255); +const Color playerColor = GameMakeColor(255,255,255,255); +const Color gameColor = GameMakeColor(255,255,255,255); +const Color gameInProgressColor = GameMakeColor(128,128,128,255); +const Color chatNormalColor = GameMakeColor(50,215,230,255); +const Color chatActionColor = GameMakeColor(255,0,255,255); +const Color chatLocalNormalColor = GameMakeColor(255,128,0,255); +const Color chatLocalActionColor = GameMakeColor(128,255,255,255); +const Color chatSystemColor = GameMakeColor(255,255,255,255); +const Color acceptTrueColor = GameMakeColor(0,255,0,255); +const Color acceptFalseColor = GameMakeColor(255,0,0,255); UnicodeString LANAPIInterface::getErrorStringFromReturnType( ReturnType ret ) @@ -646,10 +640,7 @@ void LANAPI::OnPlayerList( LANPlayer *playerList ) LANPlayer *player = m_lobbyPlayers; while (player) { - // TheSuperHackers @feature Caball009 06/11/2025 Set special color for players that are using the community patch. - const Color color = (m_localIP == player->getIP() || BitIsSet(player->getProductInfoFlags(), GameSlot::ProductInfo::NO_RETAIL)) - ? playerColorCommunityPatch : playerColor; - const Int addedIndex = GadgetListBoxAddEntryText(listboxPlayers, player->getName(), color, -1, -1); + Int addedIndex = GadgetListBoxAddEntryText(listboxPlayers, player->getName(), playerColor, -1, -1); GadgetListBoxSetItemData(listboxPlayers, (void *)player->getIP(),addedIndex, 0 ); if (selectedIP == player->getIP()) diff --git a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index 5b3a1472e50..f22e2bf249d 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -181,9 +181,6 @@ void LANAPI::handleGameProductInfoResponse(LANMessage *msg, UnsignedInt senderIP // a game host has acknowledged our request for product information setProductInfoFromMessage(msg, game->getSlot(0)); - - // update game list with colored names - OnGameList(m_games); } void LANAPI::handleLobbyProductInfoRequest(LANMessage *msg, UnsignedInt senderIP) @@ -206,9 +203,6 @@ void LANAPI::handleLobbyProductInfoResponse(LANMessage *msg, UnsignedInt senderI // a fellow player in the lobby has acknowledged our request for product information player->setProductInfoFlags(msg->ProductInfo.flags); - - // update player list with colored names - OnPlayerList(m_lobbyPlayers); } void LANAPI::handleMatchProductInfoRequest(LANMessage *msg, UnsignedInt senderIP) @@ -236,9 +230,6 @@ void LANAPI::handleMatchProductInfoResponse(LANMessage *msg, UnsignedInt senderI // a fellow player in the game has acknowledged our request for product information setProductInfoFromMessage(msg, m_currentGame->getSlot(i)); - // update player list with colored names - lanUpdateSlotList(); - break; } } diff --git a/Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp b/Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp index c55a1b6cf3f..638681e4420 100644 --- a/Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp @@ -224,12 +224,7 @@ void LANDisplayGameList( GameWindow *gameListbox, LANGameInfo *gameList ) { txtGName.concat(L"]"); } - - // TheSuperHackers @feature Caball009 06/11/2025 Set special color for games that are using the community patch. - const Color color = (BitIsSet(gameList->getSlot(0)->getProductInfo().flags, GameSlot::ProductInfo::NO_RETAIL)) - ? ((gameList->isGameInProgress()) ? gameInProgressColorCommunityPatch : gameColorCommunityPatch) - : ((gameList->isGameInProgress()) ? gameInProgressColor : gameColor); - const Int addedIndex = GadgetListBoxAddEntryText(gameListbox, txtGName, color, -1, -1); + Int addedIndex = GadgetListBoxAddEntryText(gameListbox, txtGName, (gameList->isGameInProgress())?gameInProgressColor:gameColor, -1, -1); GadgetListBoxSetItemData(gameListbox, (void *)gameList, addedIndex, 0 ); if (selectedPtr == gameList) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Diplomacy.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Diplomacy.cpp index f4ce812ad90..115a315e462 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Diplomacy.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Diplomacy.cpp @@ -36,7 +36,6 @@ #include "Common/PlayerList.h" #include "Common/PlayerTemplate.h" #include "Common/Recorder.h" -#include "Common/version.h" #include "GameClient/AnimateWindowManager.h" #include "GameClient/Diplomacy.h" #include "GameClient/DisconnectMenu.h" @@ -533,28 +532,24 @@ void PopulateInGameDiplomacyPopup( void ) if (staticTextStatus[rowNum]) { staticTextStatus[rowNum]->winHide(FALSE); - - Color frontColor = 0; - UnicodeString text; - if (isInGame) { if (isAlive) { - frontColor = aliveColor; - text = TheGameText->fetch("GUI:PlayerAlive"); + staticTextStatus[rowNum]->winSetEnabledTextColors( aliveColor, backColor ); + GadgetStaticTextSetText(staticTextStatus[rowNum], TheGameText->fetch("GUI:PlayerAlive")); } else { if (isObserver) { - frontColor = observerInGameColor; - text = TheGameText->fetch("GUI:PlayerObserver"); + staticTextStatus[rowNum]->winSetEnabledTextColors( observerInGameColor, backColor ); + GadgetStaticTextSetText(staticTextStatus[rowNum], TheGameText->fetch("GUI:PlayerObserver")); } else { - frontColor = deadColor; - text = TheGameText->fetch("GUI:PlayerDead"); + staticTextStatus[rowNum]->winSetEnabledTextColors( deadColor, backColor ); + GadgetStaticTextSetText(staticTextStatus[rowNum], TheGameText->fetch("GUI:PlayerDead")); } } } @@ -563,24 +558,15 @@ void PopulateInGameDiplomacyPopup( void ) // not in game if (isObserver) { - frontColor = observerGoneColor; - text = TheGameText->fetch("GUI:PlayerObserverGone"); + staticTextStatus[rowNum]->winSetEnabledTextColors( observerGoneColor, backColor ); + GadgetStaticTextSetText(staticTextStatus[rowNum], TheGameText->fetch("GUI:PlayerObserverGone")); } else { - frontColor = goneColor; - text = TheGameText->fetch("GUI:PlayerGone"); + staticTextStatus[rowNum]->winSetEnabledTextColors( goneColor, backColor ); + GadgetStaticTextSetText(staticTextStatus[rowNum], TheGameText->fetch("GUI:PlayerGone")); } } - - // TheSuperHackers @feature Caball009 06/11/2025 Set special status for players that are using the community patch. - if (slot->isHuman() && BitIsSet(slot->getProductInfo().flags, GameSlot::ProductInfo::NO_RETAIL)) - { - text.format(L"%s [%s]", text.str(), slot->getProductInfo().gitShortHash.str()); - } - - staticTextStatus[rowNum]->winSetEnabledTextColors(frontColor, backColor); - GadgetStaticTextSetText(staticTextStatus[rowNum], text); } slotNumInRow[rowNum++] = slotNum; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp index 645cd220059..f5ef398b0f8 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp @@ -423,17 +423,6 @@ void UpdateSlotList( GameInfo *myGame, GameWindow *comboPlayer[], } if(slot->isHuman()) { - // TheSuperHackers @feature Caball009 06/11/2025 Set special color for players that are using the community patch. - if (BitIsSet(myGame->getSlot(i)->getProductInfo().flags, GameSlot::ProductInfo::NO_RETAIL)) - { - const Bool setRegularColor = BitIsSet(myGame->getSlot(i)->getProductInfo().flags, GameSlot::ProductInfo::ZERO_MAPS_STARTED); - const Color enabledColor = (setRegularColor) ? playerColorCommunityPatch : playerColorCommunityPatchIssue; - const Color disabledColor = (setRegularColor) ? playerGrayedColorCommunityPatch : playerGrayedColorCommunityPatchIssue; - - GadgetComboBoxSetEnabledTextColors(comboPlayer[i], enabledColor, GameMakeColor(0, 0, 0, 255)); - GadgetComboBoxSetDisabledTextColors(comboPlayer[i], disabledColor, GameMakeColor(0, 0, 0, 255)); - } - UnicodeString newName = slot->getName(); UnicodeString oldName = GadgetComboBoxGetText(comboPlayer[i]); if (comboPlayer[i] && newName.compare(oldName)) From 8c71c3a615c03b3d3890a1e7fdc3da8b758e7a8f Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:31:01 +0100 Subject: [PATCH 04/17] Addressed feedback (part 1). --- .../GameEngine/Include/GameNetwork/GameInfo.h | 15 ++- Core/GameEngine/Include/GameNetwork/LANAPI.h | 14 ++- .../Include/GameNetwork/LANPlayer.h | 2 +- Core/GameEngine/Source/GameNetwork/LANAPI.cpp | 7 +- .../Source/GameNetwork/LANAPIhandlers.cpp | 114 +++++++++++------- .../GameEngine/Include/Common/GameEngine.h | 2 +- .../GameEngine/Source/Common/GameEngine.cpp | 4 +- .../Source/GameLogic/System/GameLogic.cpp | 2 +- 8 files changed, 101 insertions(+), 59 deletions(-) diff --git a/Core/GameEngine/Include/GameNetwork/GameInfo.h b/Core/GameEngine/Include/GameNetwork/GameInfo.h index a5342e9a1a6..dc04526168f 100644 --- a/Core/GameEngine/Include/GameNetwork/GameInfo.h +++ b/Core/GameEngine/Include/GameNetwork/GameInfo.h @@ -59,15 +59,23 @@ class GameSlot public: struct ProductInfo { - enum CPP_11(: UnsignedInt) + ProductInfo() : + flags(0), + launchTime(0), + exeCRC(0), + iniCRC(0), + fpMathCRC(0) + {} + + enum Flags CPP_11(: UnsignedInt) { NO_RETAIL = 1 << 0, SHELLMAP_ENABLED = 1 << 1, - ZERO_MAPS_STARTED = 1 << 2 + ZERO_MAPS_STARTED = 1 << 2, }; UnsignedInt flags; - UnsignedInt uptime; + UnsignedInt launchTime; UnsignedInt exeCRC; UnsignedInt iniCRC; UnsignedInt fpMathCRC; @@ -148,6 +156,7 @@ class GameSlot void setProductInfo(const ProductInfo& productInfo) { m_productInfo = productInfo; } const ProductInfo& getProductInfo() const { return m_productInfo; } + protected: SlotState m_state; Bool m_isAccepted; diff --git a/Core/GameEngine/Include/GameNetwork/LANAPI.h b/Core/GameEngine/Include/GameNetwork/LANAPI.h index 669ed8f6121..45772f2685e 100644 --- a/Core/GameEngine/Include/GameNetwork/LANAPI.h +++ b/Core/GameEngine/Include/GameNetwork/LANAPI.h @@ -290,12 +290,11 @@ struct LANMessage struct { UnsignedInt flags; - UnsignedInt uptime; + UnsignedInt launchTime; UnsignedInt exeCRC; UnsignedInt iniCRC; UnsignedInt fpMathCRC; WideChar data[201]; - Byte padding[20]; } ProductInfo; }; }; @@ -416,17 +415,19 @@ class LANAPI : public LANAPIInterface Bool m_isActive; ///< is the game currently active? + LANMessage m_productInfoMessage; ///< store product info message to avoid having to recreate it multiple times + protected: - void sendMessage(LANMessage *msg, UnsignedInt ip = 0); // Convenience function + void sendMessage(LANMessage *msg, UnsignedInt ip = 0, Bool broadcast = TRUE); // Convenience function void removePlayer(LANPlayer *player); void removeGame(LANGameInfo *game); void addPlayer(LANPlayer *player); void addGame(LANGameInfo *game); AsciiString createSlotString( void ); - static UnsignedInt getProductInfoFlags(); - static void setProductInfoFromLocalData(GameSlot *slot); - static void setProductInfoFromMessage(LANMessage *msg, GameSlot *slot); + static UnsignedInt buildProductInfoFlags(); + static void setProductInfoFromLocalData(GameSlot &slot); + static void setProductInfoFromMessage(GameSlot &slot, LANMessage *msg); static Bool setProductInfoStrings(const UnicodeString(&input)[4], WideChar(&output)[201]); static Bool getProductInfoStrings(WideChar(&input)[201], UnicodeString*(&output)[4]); @@ -448,6 +449,7 @@ class LANAPI : public LANAPIInterface void handleGameOptions( LANMessage *msg, UnsignedInt senderIP ); void handleInActive( LANMessage *msg, UnsignedInt senderIP ); + static LANMessage createProductInfoMessage(); void sendProductInfoMessage(LANMessage::Type messageType, UnsignedInt senderIP); void handleGameProductInfoRequest(LANMessage *msg, UnsignedInt senderIP); void handleGameProductInfoResponse(LANMessage *msg, UnsignedInt senderIP); diff --git a/Core/GameEngine/Include/GameNetwork/LANPlayer.h b/Core/GameEngine/Include/GameNetwork/LANPlayer.h index 90ef0553919..a70d48b9c58 100644 --- a/Core/GameEngine/Include/GameNetwork/LANPlayer.h +++ b/Core/GameEngine/Include/GameNetwork/LANPlayer.h @@ -52,8 +52,8 @@ class LANPlayer void setNext( LANPlayer *next ) { m_next = next; } UnsignedInt getIP( void ) { return m_IP; } void setIP( UnsignedInt IP ) { m_IP = IP; } - void setProductInfoFlags(UnsignedInt productInfoFlags) { m_productInfoFlags = productInfoFlags; } UnsignedInt getProductInfoFlags() const { return m_productInfoFlags; } + void setProductInfoFlags(UnsignedInt productInfoFlags) { m_productInfoFlags = productInfoFlags; } protected: UnicodeString m_name; ///< Player name diff --git a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp index 4dd7889e13c..c2e6e1f4f29 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp @@ -87,6 +87,7 @@ LANAPI::LANAPI( void ) : m_transport(nullptr) m_lastUpdate = 0; m_transport = new Transport; m_isActive = TRUE; + m_productInfoMessage = createProductInfoMessage(); } LANAPI::~LANAPI( void ) @@ -179,13 +180,13 @@ void LANAPI::reset( void ) } -void LANAPI::sendMessage(LANMessage *msg, UnsignedInt ip /* = 0 */) +void LANAPI::sendMessage(LANMessage *msg, UnsignedInt ip /* = 0 */, Bool broadcast /*= TRUE*/) { if (ip != 0) { m_transport->queueSend(ip, lobbyPort, (unsigned char *)msg, sizeof(LANMessage) /*, 0, 0 */); } - else if ((m_currentGame != nullptr) && (m_currentGame->getIsDirectConnect())) + else if (m_currentGame != nullptr && (m_currentGame->getIsDirectConnect() || !broadcast)) { Int localSlot = m_currentGame->getLocalSlotNum(); for (Int i = 0; i < MAX_SLOTS; ++i) @@ -927,7 +928,7 @@ void LANAPI::RequestGameCreate( UnicodeString gameName, Bool isDirectConnect ) newSlot.setHost(m_hostName); // set product information for local game slot - setProductInfoFromLocalData(&newSlot); + setProductInfoFromLocalData(newSlot); myGame->setSlot(0,newSlot); myGame->setNext(nullptr); diff --git a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index f22e2bf249d..378853e4112 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -31,67 +31,85 @@ #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine #include "Common/crc.h" +#include "Common/GameEngine.h" #include "Common/GameState.h" #include "Common/Registry.h" #include "Common/GlobalData.h" #include "Common/QuotedPrintable.h" #include "Common/UserPreferences.h" -#include "Common/GameEngine.h" #include "Common/version.h" #include "GameNetwork/LANAPI.h" #include "GameNetwork/LANAPICallbacks.h" #include "GameClient/MapUtil.h" #include "GameLogic/GameLogic.h" -Bool LANAPI::setProductInfoStrings(const UnicodeString(&input)[4], WideChar(&output)[201]) +template size_t etx_strlen_t(const T* str) { - // concatenate strings separated by null terminators + const T* begin = str; + while (*str != '\0' && *str != '\3') + ++str; + return static_cast(str - begin); +} - // null terminate the output buffer to mirror the get function - output[ARRAY_SIZE(output) - 1] = 0; +Bool LANAPI::setProductInfoStrings(const UnicodeString(&input)[4], WideChar(&output)[201]) +{ + // concatenate strings separated by end of text characters - size_t totalSize = 0; + size_t outputIndex = 0; for (size_t i = 0; i < ARRAY_SIZE(input); ++i) { - totalSize += wcslcpy(output + totalSize, input[i].str(), ARRAY_SIZE(output) - totalSize) + 1; - if (totalSize >= ARRAY_SIZE(output)) - { - return FALSE; - } + outputIndex += wcslcpy(output + outputIndex, input[i].str(), ARRAY_SIZE(output) - outputIndex); + if (outputIndex >= ARRAY_SIZE(output)) + break; + + if (i == ARRAY_SIZE(input) - 1) + break; + + output[outputIndex++] = '\3'; } - return totalSize <= ARRAY_SIZE(output); + // null terminate the output buffer to signal the end of the last input string + if (outputIndex < ARRAY_SIZE(output)) + { + output[outputIndex++] = '\0'; + return TRUE; + } + else + { + output[ARRAY_SIZE(output) - 1] = '\0'; + return FALSE; + } } Bool LANAPI::getProductInfoStrings(WideChar(&input)[201], UnicodeString*(&output)[4]) { - // extract strings separated by null terminators + // extract strings separated by end of text characters // null terminate the input buffer to prevent potential out-of-bound reads - const Bool nullTerminated = !static_cast(input[ARRAY_SIZE(input) - 1]); - input[ARRAY_SIZE(input) - 1] = 0; + input[ARRAY_SIZE(input) - 1] = '\0'; - for (size_t totalSize = 0, i = 0; i < ARRAY_SIZE(output); ++i) + size_t inputIndex = 0; + for (size_t i = 0; i < ARRAY_SIZE(output); ++i) { - const size_t size = wcslen(input + totalSize); - output[i]->set(input + totalSize, size); + const size_t length = etx_strlen_t(input + inputIndex); + output[i]->set(input + inputIndex, length); - totalSize += size + 1; - if (!nullTerminated && totalSize >= ARRAY_SIZE(input)) + inputIndex += length + 1; + if (inputIndex >= ARRAY_SIZE(input)) { for (size_t j = i + 1; j < ARRAY_SIZE(output); ++j) { output[j]->clear(); } - return FALSE; + break; } } - return TRUE; + return inputIndex <= ARRAY_SIZE(input); } -UnsignedInt LANAPI::getProductInfoFlags() +UnsignedInt LANAPI::buildProductInfoFlags() { const UnsignedInt flags = GameSlot::ProductInfo::NO_RETAIL | (GameSlot::ProductInfo::SHELLMAP_ENABLED * TheGlobalData->m_shellMapOn) @@ -100,11 +118,11 @@ UnsignedInt LANAPI::getProductInfoFlags() return flags; } -void LANAPI::setProductInfoFromLocalData(GameSlot *slot) +void LANAPI::setProductInfoFromLocalData(GameSlot &slot) { GameSlot::ProductInfo productInfo; - productInfo.flags = getProductInfoFlags(); - productInfo.uptime = TheGameEngine->getUptime(); + productInfo.flags = buildProductInfoFlags(); + productInfo.launchTime = TheGameEngine->getLaunchTime(); productInfo.exeCRC = TheGlobalData->m_exeCRC; productInfo.iniCRC = TheGlobalData->m_iniCRC; productInfo.fpMathCRC = 0; // needs to be replaced with SimulationMathCrc::calculate() from #2100 @@ -113,14 +131,14 @@ void LANAPI::setProductInfoFromLocalData(GameSlot *slot) productInfo.productAuthor = TheVersion->getUnicodeProductAuthor(); productInfo.gitShortHash = TheVersion->getUnicodeGitShortHash(); - slot->setProductInfo(productInfo); + slot.setProductInfo(productInfo); } -void LANAPI::setProductInfoFromMessage(LANMessage *msg, GameSlot *slot) +void LANAPI::setProductInfoFromMessage(GameSlot &slot, LANMessage *msg) { GameSlot::ProductInfo productInfo; productInfo.flags = msg->ProductInfo.flags; - productInfo.uptime = msg->ProductInfo.uptime; + productInfo.launchTime = msg->ProductInfo.launchTime; productInfo.exeCRC = msg->ProductInfo.exeCRC; productInfo.iniCRC = msg->ProductInfo.iniCRC; productInfo.fpMathCRC = msg->ProductInfo.fpMathCRC; @@ -134,17 +152,13 @@ void LANAPI::setProductInfoFromMessage(LANMessage *msg, GameSlot *slot) }; getProductInfoStrings(msg->ProductInfo.data, strings); - slot->setProductInfo(productInfo); + slot.setProductInfo(productInfo); } -void LANAPI::sendProductInfoMessage(LANMessage::Type messageType, UnsignedInt senderIP) +LANMessage LANAPI::createProductInfoMessage() { LANMessage msg; - fillInLANMessage(&msg); - msg.messageType = messageType; - - msg.ProductInfo.flags = getProductInfoFlags(); - msg.ProductInfo.uptime = TheGameEngine->getUptime(); + msg.ProductInfo.launchTime = TheGameEngine->getLaunchTime(); msg.ProductInfo.exeCRC = TheGlobalData->m_exeCRC; msg.ProductInfo.iniCRC = TheGlobalData->m_iniCRC; msg.ProductInfo.fpMathCRC = 0; // needs to be replaced with SimulationMathCrc::calculate() from #2100 @@ -158,7 +172,23 @@ void LANAPI::sendProductInfoMessage(LANMessage::Type messageType, UnsignedInt se }; setProductInfoStrings(strings, msg.ProductInfo.data); - sendMessage(&msg, senderIP); + return msg; +} + +void LANAPI::sendProductInfoMessage(LANMessage::Type messageType, UnsignedInt senderIP) +{ + fillInLANMessage(&m_productInfoMessage); + m_productInfoMessage.messageType = messageType; + m_productInfoMessage.ProductInfo.flags = buildProductInfoFlags(); + + if (senderIP != 0) + { + sendMessage(&m_productInfoMessage, senderIP); + } + else + { + sendMessage(&m_productInfoMessage, 0, FALSE); + } } void LANAPI::handleGameProductInfoRequest(LANMessage *msg, UnsignedInt senderIP) @@ -180,7 +210,7 @@ void LANAPI::handleGameProductInfoResponse(LANMessage *msg, UnsignedInt senderIP return; // a game host has acknowledged our request for product information - setProductInfoFromMessage(msg, game->getSlot(0)); + setProductInfoFromMessage(*game->getSlot(0), msg); } void LANAPI::handleLobbyProductInfoRequest(LANMessage *msg, UnsignedInt senderIP) @@ -228,7 +258,7 @@ void LANAPI::handleMatchProductInfoResponse(LANMessage *msg, UnsignedInt senderI continue; // a fellow player in the game has acknowledged our request for product information - setProductInfoFromMessage(msg, m_currentGame->getSlot(i)); + setProductInfoFromMessage(*m_currentGame->getSlot(i), msg); break; } @@ -364,7 +394,7 @@ void LANAPI::handleLobbyAnnounce( LANMessage *msg, UnsignedInt senderIP ) player = NEW LANPlayer; player->setIP(senderIP); - // request a player in the lobby to send product information + // request this player in the lobby to send product information sendProductInfoMessage(LANMessage::MSG_LOBBY_REQUEST_PRODUCT_INFO, senderIP); } else @@ -653,7 +683,7 @@ void LANAPI::handleJoinAccept( LANMessage *msg, UnsignedInt senderIP ) slot.setHost(m_hostName); // set product information for local game slot - setProductInfoFromLocalData(&slot); + setProductInfoFromLocalData(slot); m_currentGame->setSlot(pos, slot); @@ -670,7 +700,7 @@ void LANAPI::handleJoinAccept( LANMessage *msg, UnsignedInt senderIP ) //DEBUG_ASSERTCRASH(false, ("setting host to %ls@%ls", m_currentGame->getLANSlot(0)->getUser()->getLogin().str(), // m_currentGame->getLANSlot(0)->getUser()->getHost().str())); - // request players in the match to send product information + // request all players in the match to send product information sendProductInfoMessage(LANMessage::MSG_MATCH_REQUEST_PRODUCT_INFO, 0); } m_pendingAction = ACT_NONE; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h index b7f5ea1f9a3..0e639d01cb5 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h @@ -75,7 +75,7 @@ class GameEngine : public SubsystemInterface virtual Bool isActive(void) {return m_isActive;} ///< returns whether app has OS focus. virtual void setIsActive(Bool isActive) { m_isActive = isActive; }; - UnsignedInt getUptime() const; ///< returns time since engine creation + UnsignedInt getLaunchTime() const; ///< returns the system time when the game engine was created protected: diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index b6de75fc180..48b52893a69 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -1031,9 +1031,9 @@ Bool GameEngine::isMultiplayerSession( void ) } //------------------------------------------------------------------------------------------------- -UnsignedInt GameEngine::getUptime() const +UnsignedInt GameEngine::getLaunchTime() const { - return ::timeGetTime() - m_launchTime; + return m_launchTime; } //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 5b5b832c075..c0e1d12da9c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -1115,7 +1115,7 @@ void GameLogic::startNewGame( Bool loadingSaveGame ) // reset the frame counter m_frame = 0; m_hasUpdated = FALSE; - m_startedGamesCount += (m_gameMode != GAME_SHELL); + m_startedGamesCount += isInInteractiveGame(m_gameMode); #ifdef DEBUG_CRC // TheSuperHackers @info helmutbuhler 04/09/2025 From e83460498296d5f464178f93212fb7278e708a2c Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:00:40 +0100 Subject: [PATCH 05/17] Updated comment for improved accuracy. --- Core/GameEngine/Include/GameNetwork/LANAPI.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Core/GameEngine/Include/GameNetwork/LANAPI.h b/Core/GameEngine/Include/GameNetwork/LANAPI.h index 45772f2685e..9442ef9bf5f 100644 --- a/Core/GameEngine/Include/GameNetwork/LANAPI.h +++ b/Core/GameEngine/Include/GameNetwork/LANAPI.h @@ -178,13 +178,13 @@ struct LANMessage // A client is considered 'patched' if it responds to a product info request (or, in pre-match, if it sends one). // The implementation consists of three parts. // 1. player - player in lobby: - // - When a player detects a new player in the lobby, it sends a product info request. + // - When a player detects a new player in the lobby, it sends a product info request to that player. // - If the other player responds with an acknowledgement, they are considered patched. // 2. player - host in lobby: - // - When a player detects a new game host in the lobby, it sends a product info request. + // - When a player detects a new game host in the lobby, it sends a product info request to that host. // - If the host responds with an acknowledgement, it is considered patched. // 3. players in pre-match: - // - When a player joins a match, it sends a product info request to all existing players. + // - When a player joins a match, it sends a product info request to all existing players in that match. // - Existing players treat this request as confirmation that the joining player is patched (no explicit acknowledgement required). MSG_GAME_REQUEST_PRODUCT_INFO = 1000, MSG_GAME_RESPONSE_PRODUCT_INFO, From 7a86f434c6cc2f2665609bda57147cc0f7ce87d8 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:01:47 +0100 Subject: [PATCH 06/17] Removed temporary flags variable. --- Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index 378853e4112..5c76d46b48a 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -111,11 +111,9 @@ Bool LANAPI::getProductInfoStrings(WideChar(&input)[201], UnicodeString*(&output UnsignedInt LANAPI::buildProductInfoFlags() { - const UnsignedInt flags = GameSlot::ProductInfo::NO_RETAIL + return GameSlot::ProductInfo::NO_RETAIL | (GameSlot::ProductInfo::SHELLMAP_ENABLED * TheGlobalData->m_shellMapOn) | (GameSlot::ProductInfo::ZERO_MAPS_STARTED * (TheGameLogic->getStartedGamesCount() == 0)); - - return flags; } void LANAPI::setProductInfoFromLocalData(GameSlot &slot) From 51636b68725acff704772f060dc6a9485636f7b5 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:04:15 +0100 Subject: [PATCH 07/17] Improved comment. --- Core/GameEngine/Include/GameNetwork/LANAPI.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/GameEngine/Include/GameNetwork/LANAPI.h b/Core/GameEngine/Include/GameNetwork/LANAPI.h index 9442ef9bf5f..962e2f81253 100644 --- a/Core/GameEngine/Include/GameNetwork/LANAPI.h +++ b/Core/GameEngine/Include/GameNetwork/LANAPI.h @@ -184,7 +184,7 @@ struct LANMessage // - When a player detects a new game host in the lobby, it sends a product info request to that host. // - If the host responds with an acknowledgement, it is considered patched. // 3. players in pre-match: - // - When a player joins a match, it sends a product info request to all existing players in that match. + // - When a player joins a match, it sends a product info request to all players in that match. // - Existing players treat this request as confirmation that the joining player is patched (no explicit acknowledgement required). MSG_GAME_REQUEST_PRODUCT_INFO = 1000, MSG_GAME_RESPONSE_PRODUCT_INFO, From fd39ae4c69831c8ac7526cbc88ca8341ac86764a Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:14:30 +0100 Subject: [PATCH 08/17] Added assertion for game slot transition. --- Core/GameEngine/Source/GameNetwork/GameInfo.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Core/GameEngine/Source/GameNetwork/GameInfo.cpp b/Core/GameEngine/Source/GameNetwork/GameInfo.cpp index 9244d2a0f23..35fd067a89d 100644 --- a/Core/GameEngine/Source/GameNetwork/GameInfo.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameInfo.cpp @@ -1497,7 +1497,10 @@ Bool ParseAsciiStringToGameInfo(GameInfo *game, AsciiString options) { // retain the product information if a slot is still occupied by the same player if (game->getConstSlot(i)->getState() == SLOT_PLAYER && newSlot[i].getState() == SLOT_PLAYER) + { + DEBUG_ASSERTCRASH(game->getConstSlot(i)->getIP() == newSlot[i].getIP(), "Game slot transition was unexpected"); newSlot[i].setProductInfo(game->getConstSlot(i)->getProductInfo()); + } game->setSlot(i,newSlot[i]); } From 391bdf7e608cc19af1961b6f0ba75e57989834a8 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:49:10 +0100 Subject: [PATCH 09/17] Added parentheses around assertion comment. --- Core/GameEngine/Source/GameNetwork/GameInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/GameEngine/Source/GameNetwork/GameInfo.cpp b/Core/GameEngine/Source/GameNetwork/GameInfo.cpp index 35fd067a89d..0ce89697486 100644 --- a/Core/GameEngine/Source/GameNetwork/GameInfo.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameInfo.cpp @@ -1498,7 +1498,7 @@ Bool ParseAsciiStringToGameInfo(GameInfo *game, AsciiString options) // retain the product information if a slot is still occupied by the same player if (game->getConstSlot(i)->getState() == SLOT_PLAYER && newSlot[i].getState() == SLOT_PLAYER) { - DEBUG_ASSERTCRASH(game->getConstSlot(i)->getIP() == newSlot[i].getIP(), "Game slot transition was unexpected"); + DEBUG_ASSERTCRASH(game->getConstSlot(i)->getIP() == newSlot[i].getIP(), ("Game slot transition was unexpected")); newSlot[i].setProductInfo(game->getConstSlot(i)->getProductInfo()); } From 4302707c6138bc4165ecd9004c9865f68ae50825 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:16:15 +0100 Subject: [PATCH 10/17] Synchronized comment with comment in PR description. --- Core/GameEngine/Include/GameNetwork/LANAPI.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/GameEngine/Include/GameNetwork/LANAPI.h b/Core/GameEngine/Include/GameNetwork/LANAPI.h index 962e2f81253..22bb4f7d0a1 100644 --- a/Core/GameEngine/Include/GameNetwork/LANAPI.h +++ b/Core/GameEngine/Include/GameNetwork/LANAPI.h @@ -183,7 +183,7 @@ struct LANMessage // 2. player - host in lobby: // - When a player detects a new game host in the lobby, it sends a product info request to that host. // - If the host responds with an acknowledgement, it is considered patched. - // 3. players in pre-match: + // 3. players in pre-match (game room): // - When a player joins a match, it sends a product info request to all players in that match. // - Existing players treat this request as confirmation that the joining player is patched (no explicit acknowledgement required). MSG_GAME_REQUEST_PRODUCT_INFO = 1000, From ae118520012cb641ae330f34d82e76b8cab2b7a1 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:25:53 +0100 Subject: [PATCH 11/17] Simplified return logic for 'setProductInfoStrings'. --- .../Source/GameNetwork/LANAPIhandlers.cpp | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index a3fc58e1d51..c2fa84f491c 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -55,12 +55,11 @@ Bool LANAPI::setProductInfoStrings(const UnicodeString(&input)[4], WideChar(&out { // concatenate strings separated by end of text characters - size_t outputIndex = 0; - for (size_t i = 0; i < ARRAY_SIZE(input); ++i) + for (size_t i = 0, outputIndex = 0; i < ARRAY_SIZE(input); ++i) { outputIndex += wcslcpy(output + outputIndex, input[i].str(), ARRAY_SIZE(output) - outputIndex); if (outputIndex >= ARRAY_SIZE(output)) - break; + return FALSE; if (i == ARRAY_SIZE(input) - 1) break; @@ -68,17 +67,7 @@ Bool LANAPI::setProductInfoStrings(const UnicodeString(&input)[4], WideChar(&out output[outputIndex++] = '\3'; } - // null terminate the output buffer to signal the end of the last input string - if (outputIndex < ARRAY_SIZE(output)) - { - output[outputIndex++] = '\0'; - return TRUE; - } - else - { - output[ARRAY_SIZE(output) - 1] = '\0'; - return FALSE; - } + return TRUE; } Bool LANAPI::getProductInfoStrings(WideChar(&input)[201], UnicodeString*(&output)[4]) From 1900b24c96d4b6b1e5fb6041f4fb57ddb9a13101 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:31:18 +0100 Subject: [PATCH 12/17] Set null-terminated flag. --- .../Source/GameNetwork/LANAPIhandlers.cpp | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index c2fa84f491c..f6085b4a7af 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -43,12 +43,25 @@ #include "GameClient/MapUtil.h" #include "GameLogic/GameLogic.h" -template size_t etx_strlen_t(const T* str) +template size_t etx_strlen_t(const T* str, Bool& nullTerminated) { const T* begin = str; - while (*str != '\0' && *str != '\3') + while (true) + { + if (*str == '\0') + { + nullTerminated = TRUE; + return static_cast(str - begin); + } + + if (*str == '\3') + { + nullTerminated = FALSE; + return static_cast(str - begin); + } + ++str; - return static_cast(str - begin); + } } Bool LANAPI::setProductInfoStrings(const UnicodeString(&input)[4], WideChar(&output)[201]) @@ -80,11 +93,13 @@ Bool LANAPI::getProductInfoStrings(WideChar(&input)[201], UnicodeString*(&output size_t inputIndex = 0; for (size_t i = 0; i < ARRAY_SIZE(output); ++i) { - const size_t length = etx_strlen_t(input + inputIndex); + Bool nullTerminated = FALSE; + const size_t length = etx_strlen_t(input + inputIndex, nullTerminated); output[i]->set(input + inputIndex, length); inputIndex += length + 1; - if (inputIndex >= ARRAY_SIZE(input)) + + if (nullTerminated) { for (size_t j = i + 1; j < ARRAY_SIZE(output); ++j) { From 870bfd3d066634c115f718dde7896382d40742c5 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:31:43 +0100 Subject: [PATCH 13/17] Simplified return logic for 'getProductInfoStrings'. --- Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index f6085b4a7af..1ca60cf5050 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -90,8 +90,7 @@ Bool LANAPI::getProductInfoStrings(WideChar(&input)[201], UnicodeString*(&output // null terminate the input buffer to prevent potential out-of-bound reads input[ARRAY_SIZE(input) - 1] = '\0'; - size_t inputIndex = 0; - for (size_t i = 0; i < ARRAY_SIZE(output); ++i) + for (size_t i = 0, inputIndex = 0; i < ARRAY_SIZE(output); ++i) { Bool nullTerminated = FALSE; const size_t length = etx_strlen_t(input + inputIndex, nullTerminated); @@ -106,11 +105,11 @@ Bool LANAPI::getProductInfoStrings(WideChar(&input)[201], UnicodeString*(&output output[j]->clear(); } - break; + return TRUE; } } - return inputIndex <= ARRAY_SIZE(input); + return FALSE; } UnsignedInt LANAPI::buildProductInfoFlags() From 759efbc313de24f3ee0a34f63f3c51b3e3e8edcf Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:40:20 +0100 Subject: [PATCH 14/17] Changed 'createProductInfoMessage' to 'buildProductInfoMessage'. --- Core/GameEngine/Include/GameNetwork/LANAPI.h | 2 +- Core/GameEngine/Source/GameNetwork/LANAPI.cpp | 2 +- Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Core/GameEngine/Include/GameNetwork/LANAPI.h b/Core/GameEngine/Include/GameNetwork/LANAPI.h index 6791a035cb7..e1c54868d1d 100644 --- a/Core/GameEngine/Include/GameNetwork/LANAPI.h +++ b/Core/GameEngine/Include/GameNetwork/LANAPI.h @@ -449,7 +449,7 @@ class LANAPI : public LANAPIInterface void handleGameOptions( LANMessage *msg, UnsignedInt senderIP ); void handleInActive( LANMessage *msg, UnsignedInt senderIP ); - static LANMessage createProductInfoMessage(); + static LANMessage buildProductInfoMessage(); void sendProductInfoMessage(LANMessage::Type messageType, UnsignedInt senderIP); void handleGameProductInfoRequest(LANMessage *msg, UnsignedInt senderIP); void handleGameProductInfoResponse(LANMessage *msg, UnsignedInt senderIP); diff --git a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp index 79fc7d42fea..8970408d3be 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp @@ -87,7 +87,7 @@ LANAPI::LANAPI() : m_transport(nullptr) m_lastUpdate = 0; m_transport = new Transport; m_isActive = TRUE; - m_productInfoMessage = createProductInfoMessage(); + m_productInfoMessage = buildProductInfoMessage(); } LANAPI::~LANAPI() diff --git a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index 1ca60cf5050..5915ef54d37 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -156,7 +156,7 @@ void LANAPI::setProductInfoFromMessage(GameSlot &slot, LANMessage *msg) slot.setProductInfo(productInfo); } -LANMessage LANAPI::createProductInfoMessage() +LANMessage LANAPI::buildProductInfoMessage() { LANMessage msg; msg.ProductInfo.launchTime = TheGameEngine->getLaunchTime(); From d53d412f305aadbb966142e8da7b7d0afb854829 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:11:03 +0100 Subject: [PATCH 15/17] Set math crc field. --- Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index 5915ef54d37..19795e20f8e 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -31,6 +31,7 @@ #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine #include "Common/crc.h" +#include "Common/Diagnostic/SimulationMathCrc.h" #include "Common/GameEngine.h" #include "Common/GameState.h" #include "Common/Registry.h" @@ -126,7 +127,7 @@ void LANAPI::setProductInfoFromLocalData(GameSlot &slot) productInfo.launchTime = TheGameEngine->getLaunchTime(); productInfo.exeCRC = TheGlobalData->m_exeCRC; productInfo.iniCRC = TheGlobalData->m_iniCRC; - productInfo.fpMathCRC = 0; // needs to be replaced with SimulationMathCrc::calculate() from #2100 + productInfo.fpMathCRC = SimulationMathCrc::calculate(); productInfo.productTitle = TheVersion->getUnicodeProductTitle(); productInfo.productVersion = TheVersion->getUnicodeProductVersion(); productInfo.productAuthor = TheVersion->getUnicodeProductAuthor(); @@ -162,7 +163,7 @@ LANMessage LANAPI::buildProductInfoMessage() msg.ProductInfo.launchTime = TheGameEngine->getLaunchTime(); msg.ProductInfo.exeCRC = TheGlobalData->m_exeCRC; msg.ProductInfo.iniCRC = TheGlobalData->m_iniCRC; - msg.ProductInfo.fpMathCRC = 0; // needs to be replaced with SimulationMathCrc::calculate() from #2100 + msg.ProductInfo.fpMathCRC = SimulationMathCrc::calculate(); const UnicodeString strings[] = { From a8e1aa2dc3734e3a3e8ba4234bfa2a3e59bcf48a Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:11:49 +0100 Subject: [PATCH 16/17] Switched back from launch time to up time. --- Core/GameEngine/Include/GameNetwork/GameInfo.h | 4 ++-- Core/GameEngine/Include/GameNetwork/LANAPI.h | 2 +- Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp | 6 +++--- GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h | 1 + GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp | 6 ++++++ 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Core/GameEngine/Include/GameNetwork/GameInfo.h b/Core/GameEngine/Include/GameNetwork/GameInfo.h index 61aab7b483e..69b9eb98dfe 100644 --- a/Core/GameEngine/Include/GameNetwork/GameInfo.h +++ b/Core/GameEngine/Include/GameNetwork/GameInfo.h @@ -61,7 +61,7 @@ class GameSlot { ProductInfo() : flags(0), - launchTime(0), + upTime(0), exeCRC(0), iniCRC(0), fpMathCRC(0) @@ -75,7 +75,7 @@ class GameSlot }; UnsignedInt flags; - UnsignedInt launchTime; + UnsignedInt upTime; UnsignedInt exeCRC; UnsignedInt iniCRC; UnsignedInt fpMathCRC; diff --git a/Core/GameEngine/Include/GameNetwork/LANAPI.h b/Core/GameEngine/Include/GameNetwork/LANAPI.h index e1c54868d1d..a19a9b1925e 100644 --- a/Core/GameEngine/Include/GameNetwork/LANAPI.h +++ b/Core/GameEngine/Include/GameNetwork/LANAPI.h @@ -290,7 +290,7 @@ struct LANMessage struct { UnsignedInt flags; - UnsignedInt launchTime; + UnsignedInt upTime; UnsignedInt exeCRC; UnsignedInt iniCRC; UnsignedInt fpMathCRC; diff --git a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index 19795e20f8e..50455ff56be 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -124,7 +124,7 @@ void LANAPI::setProductInfoFromLocalData(GameSlot &slot) { GameSlot::ProductInfo productInfo; productInfo.flags = buildProductInfoFlags(); - productInfo.launchTime = TheGameEngine->getLaunchTime(); + productInfo.upTime = TheGameEngine->getUpTime(); productInfo.exeCRC = TheGlobalData->m_exeCRC; productInfo.iniCRC = TheGlobalData->m_iniCRC; productInfo.fpMathCRC = SimulationMathCrc::calculate(); @@ -140,7 +140,7 @@ void LANAPI::setProductInfoFromMessage(GameSlot &slot, LANMessage *msg) { GameSlot::ProductInfo productInfo; productInfo.flags = msg->ProductInfo.flags; - productInfo.launchTime = msg->ProductInfo.launchTime; + productInfo.upTime = msg->ProductInfo.upTime; productInfo.exeCRC = msg->ProductInfo.exeCRC; productInfo.iniCRC = msg->ProductInfo.iniCRC; productInfo.fpMathCRC = msg->ProductInfo.fpMathCRC; @@ -160,7 +160,6 @@ void LANAPI::setProductInfoFromMessage(GameSlot &slot, LANMessage *msg) LANMessage LANAPI::buildProductInfoMessage() { LANMessage msg; - msg.ProductInfo.launchTime = TheGameEngine->getLaunchTime(); msg.ProductInfo.exeCRC = TheGlobalData->m_exeCRC; msg.ProductInfo.iniCRC = TheGlobalData->m_iniCRC; msg.ProductInfo.fpMathCRC = SimulationMathCrc::calculate(); @@ -182,6 +181,7 @@ void LANAPI::sendProductInfoMessage(LANMessage::Type messageType, UnsignedInt se fillInLANMessage(&m_productInfoMessage); m_productInfoMessage.messageType = messageType; m_productInfoMessage.ProductInfo.flags = buildProductInfoFlags(); + m_productInfoMessage.ProductInfo.upTime = TheGameEngine->getUpTime(); if (senderIP != 0) { diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h index bed91709057..26a6fbcee5c 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h @@ -76,6 +76,7 @@ class GameEngine : public SubsystemInterface virtual void setIsActive(Bool isActive) { m_isActive = isActive; }; UnsignedInt getLaunchTime() const; ///< returns the system time when the game engine was created + UnsignedInt getUpTime() const; ///< returns the period of time the game engine has been running protected: diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index 0747534fb51..792b5e2ab88 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -1036,6 +1036,12 @@ UnsignedInt GameEngine::getLaunchTime() const return m_launchTime; } +//------------------------------------------------------------------------------------------------- +UnsignedInt GameEngine::getUpTime() const +{ + return ::timeGetTime() - m_launchTime; +} + //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- From c4a34cedbfef5110c8a924f9b96ddd21dd0f8009 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:22:42 +0100 Subject: [PATCH 17/17] Fixed return logic for 'getProductInfoStrings'. --- Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index 50455ff56be..d2183e2a33f 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -101,12 +101,15 @@ Bool LANAPI::getProductInfoStrings(WideChar(&input)[201], UnicodeString*(&output if (nullTerminated) { + if (i == ARRAY_SIZE(output) - 1) + return TRUE; + for (size_t j = i + 1; j < ARRAY_SIZE(output); ++j) { output[j]->clear(); } - return TRUE; + break; } }