diff --git a/Core/GameEngine/Include/GameNetwork/GameInfo.h b/Core/GameEngine/Include/GameNetwork/GameInfo.h index d17a3bd3c12..69b9eb98dfe 100644 --- a/Core/GameEngine/Include/GameNetwork/GameInfo.h +++ b/Core/GameEngine/Include/GameNetwork/GameInfo.h @@ -57,6 +57,34 @@ enum class GameSlot { public: + struct ProductInfo + { + ProductInfo() : + flags(0), + upTime(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, + }; + + UnsignedInt flags; + UnsignedInt upTime; + UnsignedInt exeCRC; + UnsignedInt iniCRC; + UnsignedInt fpMathCRC; + UnicodeString productTitle; + UnicodeString productVersion; + UnicodeString productAuthor; + UnicodeString gitShortHash; + }; + GameSlot(); virtual void reset(); @@ -125,6 +153,10 @@ class GameSlot void mute( Bool isMuted ) { m_isMuted = isMuted; } Bool isMuted() 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 +175,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 a562aa257bd..a19a9b1925e 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 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 to that host. + // - If the host responds with an acknowledgement, it is considered patched. + // 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, + 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,16 @@ 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]; + } ProductInfo; }; }; #pragma pack(pop) @@ -386,14 +415,22 @@ 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(); + 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]); + // Functions to handle incoming messages ----------------------------------- void handleRequestLocations( LANMessage *msg, UnsignedInt senderIP ); void handleGameAnnounce( LANMessage *msg, UnsignedInt senderIP ); @@ -412,4 +449,12 @@ class LANAPI : public LANAPIInterface void handleGameOptions( LANMessage *msg, UnsignedInt senderIP ); void handleInActive( LANMessage *msg, UnsignedInt senderIP ); + static LANMessage buildProductInfoMessage(); + 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 a25b201c34a..95b51f2de56 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() { return m_name; } @@ -52,6 +52,8 @@ class LANPlayer void setNext( LANPlayer *next ) { m_next = next; } UnsignedInt getIP() { return m_IP; } void setIP( UnsignedInt IP ) { m_IP = IP; } + UnsignedInt getProductInfoFlags() const { return m_productInfoFlags; } + void setProductInfoFlags(UnsignedInt productInfoFlags) { m_productInfoFlags = 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 475f51a8683..cbe96807d2c 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() @@ -1493,7 +1494,16 @@ 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) + { + 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]); + } game->setMap(mapName); game->setMapCRC(mapCRC); diff --git a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp index 1016d26f66b..8970408d3be 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp @@ -87,6 +87,7 @@ LANAPI::LANAPI() : m_transport(nullptr) m_lastUpdate = 0; m_transport = new Transport; m_isActive = TRUE; + m_productInfoMessage = buildProductInfoMessage(); } LANAPI::~LANAPI() @@ -179,13 +180,13 @@ void LANAPI::reset() } -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) @@ -425,6 +426,26 @@ void LANAPI::update() 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 +927,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 7f88b835f86..d2183e2a33f 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -31,14 +31,243 @@ #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" #include "Common/GlobalData.h" #include "Common/QuotedPrintable.h" #include "Common/UserPreferences.h" +#include "Common/version.h" #include "GameNetwork/LANAPI.h" #include "GameNetwork/LANAPICallbacks.h" #include "GameClient/MapUtil.h" +#include "GameLogic/GameLogic.h" + +template size_t etx_strlen_t(const T* str, Bool& nullTerminated) +{ + const T* begin = str; + while (true) + { + if (*str == '\0') + { + nullTerminated = TRUE; + return static_cast(str - begin); + } + + if (*str == '\3') + { + nullTerminated = FALSE; + return static_cast(str - begin); + } + + ++str; + } +} + +Bool LANAPI::setProductInfoStrings(const UnicodeString(&input)[4], WideChar(&output)[201]) +{ + // concatenate strings separated by end of text characters + + 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)) + return FALSE; + + if (i == ARRAY_SIZE(input) - 1) + break; + + output[outputIndex++] = '\3'; + } + + return TRUE; +} + +Bool LANAPI::getProductInfoStrings(WideChar(&input)[201], UnicodeString*(&output)[4]) +{ + // extract strings separated by end of text characters + + // null terminate the input buffer to prevent potential out-of-bound reads + input[ARRAY_SIZE(input) - 1] = '\0'; + + 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); + output[i]->set(input + inputIndex, length); + + inputIndex += length + 1; + + if (nullTerminated) + { + if (i == ARRAY_SIZE(output) - 1) + return TRUE; + + for (size_t j = i + 1; j < ARRAY_SIZE(output); ++j) + { + output[j]->clear(); + } + + break; + } + } + + return FALSE; +} + +UnsignedInt LANAPI::buildProductInfoFlags() +{ + return GameSlot::ProductInfo::NO_RETAIL + | (GameSlot::ProductInfo::SHELLMAP_ENABLED * TheGlobalData->m_shellMapOn) + | (GameSlot::ProductInfo::ZERO_MAPS_STARTED * (TheGameLogic->getStartedGamesCount() == 0)); +} + +void LANAPI::setProductInfoFromLocalData(GameSlot &slot) +{ + GameSlot::ProductInfo productInfo; + productInfo.flags = buildProductInfoFlags(); + productInfo.upTime = TheGameEngine->getUpTime(); + productInfo.exeCRC = TheGlobalData->m_exeCRC; + productInfo.iniCRC = TheGlobalData->m_iniCRC; + productInfo.fpMathCRC = SimulationMathCrc::calculate(); + productInfo.productTitle = TheVersion->getUnicodeProductTitle(); + productInfo.productVersion = TheVersion->getUnicodeProductVersion(); + productInfo.productAuthor = TheVersion->getUnicodeProductAuthor(); + productInfo.gitShortHash = TheVersion->getUnicodeGitShortHash(); + + slot.setProductInfo(productInfo); +} + +void LANAPI::setProductInfoFromMessage(GameSlot &slot, LANMessage *msg) +{ + 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); +} + +LANMessage LANAPI::buildProductInfoMessage() +{ + LANMessage msg; + msg.ProductInfo.exeCRC = TheGlobalData->m_exeCRC; + msg.ProductInfo.iniCRC = TheGlobalData->m_iniCRC; + msg.ProductInfo.fpMathCRC = SimulationMathCrc::calculate(); + + const UnicodeString strings[] = + { + TheVersion->getUnicodeProductTitle(), + TheVersion->getUnicodeProductVersion(), + TheVersion->getUnicodeProductAuthor(), + TheVersion->getUnicodeGitShortHash() + }; + setProductInfoStrings(strings, msg.ProductInfo.data); + + return msg; +} + +void LANAPI::sendProductInfoMessage(LANMessage::Type messageType, UnsignedInt senderIP) +{ + fillInLANMessage(&m_productInfoMessage); + m_productInfoMessage.messageType = messageType; + m_productInfoMessage.ProductInfo.flags = buildProductInfoFlags(); + m_productInfoMessage.ProductInfo.upTime = TheGameEngine->getUpTime(); + + if (senderIP != 0) + { + sendMessage(&m_productInfoMessage, senderIP); + } + else + { + sendMessage(&m_productInfoMessage, 0, FALSE); + } +} + +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(*game->getSlot(0), msg); +} + +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(*m_currentGame->getSlot(i), msg); + + break; + } +} void LANAPI::handleRequestLocations( LANMessage *msg, UnsignedInt senderIP ) { @@ -140,6 +369,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 +398,9 @@ void LANAPI::handleLobbyAnnounce( LANMessage *msg, UnsignedInt senderIP ) { player = NEW LANPlayer; player->setIP(senderIP); + + // request this player in the lobby to send product information + sendProductInfoMessage(LANMessage::MSG_LOBBY_REQUEST_PRODUCT_INFO, senderIP); } else { @@ -451,6 +686,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 +704,9 @@ void LANAPI::handleJoinAccept( LANMessage *msg, UnsignedInt senderIP ) OnGameJoin(RET_OK, m_currentGame); //DEBUG_CRASH(("setting host to %ls@%ls", m_currentGame->getLANSlot(0)->getUser()->getLogin().str(), // m_currentGame->getLANSlot(0)->getUser()->getHost().str())); + + // request all 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 b12c83bab70..26a6fbcee5c 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h @@ -75,6 +75,9 @@ class GameEngine : public SubsystemInterface virtual Bool isActive() {return m_isActive;} ///< returns whether app has OS focus. 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: virtual void resetSubsystems(); @@ -101,6 +104,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 160d1dbd4cf..6f74ea79c79 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(); ///< 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() { 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 d90ffb6e266..792b5e2ab88 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -250,6 +250,7 @@ GameEngine::GameEngine() m_logicTimeAccumulator = 0.0f; m_quitting = FALSE; m_isActive = FALSE; + m_launchTime = ::timeGetTime(); _Module.Init(nullptr, ApplicationHInstance, nullptr); } @@ -1029,6 +1030,18 @@ Bool GameEngine::isMultiplayerSession() return TheRecorder->isMultiplayer(); } +//------------------------------------------------------------------------------------------------- +UnsignedInt GameEngine::getLaunchTime() const +{ + return m_launchTime; +} + +//------------------------------------------------------------------------------------------------- +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 1d671f180f5..4226d529d16 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() 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 += isInInteractiveGame(m_gameMode); #ifdef DEBUG_CRC // TheSuperHackers @info helmutbuhler 04/09/2025