diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Moderation.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Moderation.h index 072e9c05ebc..2e1e6439b21 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Moderation.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Moderation.h @@ -9,5 +9,6 @@ enum class EOnlineModerationAction }; void ShowLoginBanDialog(const std::string& reason); +void ShowChatRateLimitNotice(const std::string& reason, const std::string& scopeType); void HandleModerationDisconnect(EOnlineModerationAction action, const std::string& reason); void HandleModerationNotice(const std::string& actionType, const std::string& reason, const std::string& scopeType); diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.h index 6347fbaf371..7d0211b82cb 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.h @@ -16,6 +16,19 @@ enum class EChatMessageType CHAT_MESSAGE_TYPE_NETWORK_ROOM, CHAT_MESSAGE_TYPE_LOBBY }; +static Color DetermineSystemNoticeColor(bool bWarning = false, bool bError = false) +{ + if (bError) + { + return GameMakeColor(255, 94, 94, 255); + } + if (bWarning) + { + return GameMakeColor(255, 194, 15, 255); + } + return GameMakeColor(192, 192, 192, 255); +} + static Color DetermineColorForChatMessage(EChatMessageType chatMessageType, Bool isPublic, bool bAction, bool bAdmin, bool bIsNameChange, int lobbySlot = -1) { Color style = GameMakeColor(255, 255, 255, 255); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp index eb5b1ee74dc..3b353c9270c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp @@ -70,6 +70,7 @@ #include "GameNetwork/GameSpy/LobbyUtils.h" #include "GameNetwork/RankPointValue.h" #include "GameNetwork/GeneralsOnline/NGMP_interfaces.h" +#include "GameNetwork/GeneralsOnline/OnlineServices_Moderation.h" #include @@ -132,10 +133,8 @@ static Int groupRoomToJoin = 0; static Int initialGadgetDelay = 2; static Bool justEntered = FALSE; -// TODO: Remove this client-side limit after server-side rate limiting is deployed. +// Preserve rejected messages while the server enforces the limit. static std::deque s_lobbyChatMessageTimes; -static std::chrono::steady_clock::time_point s_lastLobbyChatRateLimitNotice; -static Bool s_hasShownLobbyChatRateLimitNotice = FALSE; static bool LobbyChatRateLimitAllowsSend() { @@ -150,17 +149,9 @@ static bool LobbyChatRateLimitAllowsSend() if (s_lobbyChatMessageTimes.size() >= 3) { - if (!s_hasShownLobbyChatRateLimitNotice || now - s_lastLobbyChatRateLimitNotice >= window) - { - GadgetListBoxAddEntryText( - listboxLobbyChat, - UnicodeString(L"Rate limit: Please wait before sending another message."), - GameMakeColor(255, 194, 15, 255), - -1, - -1); - s_lastLobbyChatRateLimitNotice = now; - s_hasShownLobbyChatRateLimitNotice = TRUE; - } + ShowChatRateLimitNotice( + "Rate limit: Please wait before sending another message.", + "room"); return false; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp index 6cd80ba3aea..4400da9ff7f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp @@ -154,6 +154,8 @@ static Int minPoints = 0; static Int matchFoundTimeoutStart = 0; static const Int lobbyTimeoutMs = 10000; static Int matchFoundTimeoutDurationMs = lobbyTimeoutMs; +static const Int matchStartCountdownDurationMs = 5000; +static Int matchStartCountdownLastSecond = 0; static const LadderInfo * getLadderInfo(); @@ -911,6 +913,9 @@ void WOLQuickMatchMenuInit( WindowLayout *layout, void *userData ) buttonPushed = false; isShuttingDown = false; raiseMessageBoxes = true; + matchFoundTimeoutStart = 0; + matchFoundTimeoutDurationMs = lobbyTimeoutMs; + matchStartCountdownLastSecond = 0; delete TheNAT; TheNAT = nullptr; @@ -1222,6 +1227,7 @@ void WOLQuickMatchMenuInit( WindowLayout *layout, void *userData ) buttonStop->winEnable(FALSE); matchFoundTimeoutDurationMs = lobbyTimeoutMs; matchFoundTimeoutStart = timeGetTime(); + matchStartCountdownLastSecond = 0; if (TheAudio) { AudioEventRTS evt("GUICommunicatorOpen"); @@ -1233,6 +1239,7 @@ void WOLQuickMatchMenuInit( WindowLayout *layout, void *userData ) { matchFoundTimeoutStart = 0; matchFoundTimeoutDurationMs = lobbyTimeoutMs; + matchStartCountdownLastSecond = 0; buttonBack->winEnable(TRUE); buttonStop->winEnable(TRUE); buttonWiden->winEnable(TRUE); @@ -1242,11 +1249,17 @@ void WOLQuickMatchMenuInit( WindowLayout *layout, void *userData ) { matchFoundTimeoutDurationMs = timeoutMs; matchFoundTimeoutStart = timeGetTime(); + + // Mirror the service-owned countdown for UI feedback. + matchStartCountdownLastSecond = timeoutMs < lobbyTimeoutMs + ? (matchStartCountdownDurationMs + 999) / 1000 + : 0; }); pLobbyInterface->RegisterForMatchmakingStartGameCallback([]() { matchFoundTimeoutStart = 0; + matchStartCountdownLastSecond = 0; NetworkLog(ELogVerbosity::LOG_DEBUG, "[QUICKMATCH] GOT START GAME EVENT"); // Check if TheNGMPGame is initialized before dereferencing it @@ -1490,6 +1503,8 @@ void WOLQuickMatchMenuShutdown( WindowLayout *layout, void *userData ) buttonBack = nullptr; quickmatchTextWindow = nullptr; selectedImage = unselectedImage = nullptr; + matchFoundTimeoutStart = 0; + matchStartCountdownLastSecond = 0; isShuttingDown = true; @@ -1597,7 +1612,33 @@ void WOLQuickMatchMenuUpdate( WindowLayout * layout, void *userData) HandleBuddyResponses(); #endif - if (matchFoundTimeoutStart != 0 && timeGetTime() - matchFoundTimeoutStart >= matchFoundTimeoutDurationMs) + if (matchStartCountdownLastSecond > 0) + { + Int elapsedMs = timeGetTime() - matchFoundTimeoutStart; + Int remainingMs = matchStartCountdownDurationMs - elapsedMs; + Int secondsRemaining = remainingMs > 0 ? (remainingMs + 999) / 1000 : 0; + + if (secondsRemaining > 0 && secondsRemaining < matchStartCountdownLastSecond) + { + UnicodeString countdownMessage; + if (secondsRemaining == 1) + { + countdownMessage.format(TheGameText->fetch("LAN:GameStartTimerSingular"), secondsRemaining); + } + else + { + countdownMessage.format(TheGameText->fetch("LAN:GameStartTimerPlural"), secondsRemaining); + } + + Int index = GadgetListBoxAddEntryText(quickmatchTextWindow, countdownMessage, GameMakeColor(192, 192, 192, 255), -1, -1); + GadgetListBoxSetItemData(quickmatchTextWindow, (void*)-1, index); + matchStartCountdownLastSecond = secondsRemaining; + } + } + + // Leave time for the server's START_GAME event to arrive. + Int effectiveTimeoutMs = matchFoundTimeoutDurationMs < lobbyTimeoutMs ? lobbyTimeoutMs : matchFoundTimeoutDurationMs; + if (matchFoundTimeoutStart != 0 && timeGetTime() - matchFoundTimeoutStart >= effectiveTimeoutMs) { matchFoundTimeoutStart = 0; buttonBack->winEnable(TRUE); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Moderation.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Moderation.cpp index 56e96e8d69b..b8ac3b0f197 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Moderation.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Moderation.cpp @@ -6,6 +6,8 @@ #include "GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.h" #include "GameNetwork/GameSpyOverlay.h" +#include +#include #include #include #include @@ -100,40 +102,66 @@ namespace reason, EModerationDialogContext::ACTIVE_SESSION); } +} + +void ShowLoginBanDialog(const std::string& reason) +{ + ShowBanDialog(reason, EModerationDialogContext::LOGIN); +} + +void ShowChatRateLimitNotice(const std::string& reason, const std::string& scopeType) +{ + using namespace std::chrono; - void ShowChatNotice(const std::string& reason, const std::string& scopeType) + const std::wstring normalizedReason = GetNormalizedReason(reason); + if (normalizedReason.empty()) { - const std::wstring normalizedReason = GetNormalizedReason(reason); - if (normalizedReason.empty()) + return; + } + + using NoticeKey = std::pair; + static std::map lastNotices; + + const auto now = steady_clock::now(); + const auto window = seconds(9); + for (auto it = lastNotices.begin(); it != lastNotices.end();) + { + if (now - it->second >= window) { - return; + it = lastNotices.erase(it); } - - UnicodeString message(normalizedReason.c_str()); - const Color color = GameMakeColor(255, 194, 15, 255); - if (scopeType == "lobby") + else { - NGMP_OnlineServices_LobbyInterface* lobbyInterface = - NGMP_OnlineServicesManager::GetInterface(); - if (lobbyInterface != nullptr && lobbyInterface->m_OnChatCallback != nullptr) - { - lobbyInterface->m_OnChatCallback(message, color); - } - return; + ++it; } + } - NGMP_OnlineServices_RoomsInterface* roomsInterface = - NGMP_OnlineServicesManager::GetInterface(); - if (roomsInterface != nullptr && roomsInterface->m_OnChatCallback != nullptr) + const NoticeKey key(scopeType, normalizedReason); + if (lastNotices.find(key) != lastNotices.end()) + { + return; + } + lastNotices[key] = now; + + UnicodeString message(normalizedReason.c_str()); + const Color color = DetermineSystemNoticeColor(true, false); + if (scopeType == "lobby") + { + NGMP_OnlineServices_LobbyInterface* lobbyInterface = + NGMP_OnlineServicesManager::GetInterface(); + if (lobbyInterface != nullptr && lobbyInterface->m_OnChatCallback != nullptr) { - roomsInterface->m_OnChatCallback(message, color); + lobbyInterface->m_OnChatCallback(message, color); } + return; } -} -void ShowLoginBanDialog(const std::string& reason) -{ - ShowBanDialog(reason, EModerationDialogContext::LOGIN); + NGMP_OnlineServices_RoomsInterface* roomsInterface = + NGMP_OnlineServicesManager::GetInterface(); + if (roomsInterface != nullptr && roomsInterface->m_OnChatCallback != nullptr) + { + roomsInterface->m_OnChatCallback(message, color); + } } void HandleModerationDisconnect(EOnlineModerationAction action, const std::string& reason) @@ -170,7 +198,7 @@ void HandleModerationNotice(const std::string& actionType, const std::string& re } else if (actionType == "rate_limit") { - ShowChatNotice(reason, scopeType); + ShowChatRateLimitNotice(reason, scopeType); } else { diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp index 2d0182a58c1..d2564d9cd74 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp @@ -1304,8 +1304,12 @@ void WebSocket::Tick() } } - // no admin chat in lobby - Color color = DetermineColorForChatMessage(EChatMessageType::CHAT_MESSAGE_TYPE_LOBBY, true, chatData.action, false, false, lobbySlot); + // Match local setup notice colors. + // System announcements stay neutral even when they come from the host; + // player chat keeps its per-slot color. + Color color = chatData.announcement + ? DetermineSystemNoticeColor(false, false) + : DetermineColorForChatMessage(EChatMessageType::CHAT_MESSAGE_TYPE_LOBBY, true, chatData.action, false, false, lobbySlot); if (pLobbyInterface->m_OnChatCallback != nullptr) {