diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameInfo.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameInfo.cpp index 273bdbc5dc..ff035cd3dc 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameInfo.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameInfo.cpp @@ -44,6 +44,8 @@ #include "GameNetwork/LANAPI.h" // for testing packet size #include "GameNetwork/LANAPICallbacks.h" // for testing packet size #include "strtok_r.h" +#include +#include @@ -892,7 +894,100 @@ Bool GameInfo::isSandbox(void) static const char slotListID = 'S'; -AsciiString GameInfoToAsciiString( const GameInfo *game ) +struct LengthIndexPair +{ + Int Length; + size_t Index; + friend bool operator<(const LengthIndexPair& lhs, const LengthIndexPair& rhs) + { + if (lhs.Length == rhs.Length) + return lhs.Index < rhs.Index; + return lhs.Length < rhs.Length; + } + friend bool operator>(const LengthIndexPair& lhs, const LengthIndexPair& rhs) { return rhs < lhs; } + friend bool operator<=(const LengthIndexPair& lhs, const LengthIndexPair& rhs) { return !(lhs > rhs); } + friend bool operator>=(const LengthIndexPair& lhs, const LengthIndexPair& rhs) { return !(lhs < rhs); } +}; + +static AsciiStringVec BuildPlayerNames(const GameInfo& game) +{ + AsciiStringVec playerNames; + playerNames.resize(MAX_SLOTS); + + for (Int i = 0; i < MAX_SLOTS; ++i) + { + const GameSlot* slot = game.getConstSlot(i); + if (slot->isHuman()) + { + playerNames[i] = WideCharStringToMultiByte(slot->getName().str()).c_str(); + } + } + + return playerNames; +} + +static Bool TruncatePlayerNames(AsciiStringVec& playerNames, UnsignedInt truncateAmount) +{ + // wont truncate any name to below this length + CONSTEXPR const Int MinimumNameLength = 2; + UnsignedInt availableForTruncation = 0; + + // make length+index pairs for the player names + std::vector lengthIndex; + lengthIndex.resize(playerNames.size()); + for (size_t pi = 0; pi < playerNames.size(); ++pi) + { + Int playerNameLength = playerNames[pi].getLength(); + lengthIndex[pi].Length = playerNameLength; + lengthIndex[pi].Index = pi; + availableForTruncation += std::max(0, playerNameLength - MinimumNameLength); + } + + if (truncateAmount > availableForTruncation) + { + DEBUG_LOG(("TruncatePlayerNames - Requested to truncate %u chars from player names, but only %u were available for truncation.", truncateAmount, availableForTruncation)); + return false; + } + + // sort based on length in descending order + std::sort(lengthIndex.begin(), lengthIndex.end(), std::greater()); + + for (size_t i = 0; i < lengthIndex.size(); ++i) + { + size_t remainingEntries = lengthIndex.size() - i; + // determine average length based on the total amount of characters available for truncation and how many are remaining to be removed + int avgLengthForRemaining = ((availableForTruncation - truncateAmount) + (remainingEntries * MinimumNameLength)) / remainingEntries; + if (lengthIndex[i].Length > avgLengthForRemaining) + { + int truncateCurrent = lengthIndex[i].Length - avgLengthForRemaining; + playerNames[lengthIndex[i].Index].truncateBy(truncateCurrent); + truncateAmount -= truncateCurrent; + } + + availableForTruncation -= lengthIndex[i].Length - MinimumNameLength; + + if (truncateAmount == 0) + { + break; + } + } + + // ensure there are no duplicates in the truncated names + std::set uniqueNames; + for (size_t ni = 0; ni < playerNames.size(); ++ni) + { + while (!uniqueNames.insert(playerNames[ni]).second) + { + // the name already exists, change the last char to a random between a and z to ensure differentiation + playerNames[ni].removeLastChar(); + playerNames[ni].concat(GameClientRandomValue('a', 'z')); + } + } + + return true; +} + +AsciiString GameInfoToAsciiString(const GameInfo *game, const AsciiStringVec& playerNames) { if (!game) return AsciiString::TheEmptyString; @@ -918,7 +1013,7 @@ AsciiString GameInfoToAsciiString( const GameInfo *game ) newMapName.concat(token); mapName.nextToken(&token, "\\/"); } - DEBUG_LOG(("Map name is %s", mapName.str())); + DEBUG_LOG(("Map name is %s", newMapName.str())); } AsciiString optionsString; @@ -936,23 +1031,13 @@ AsciiString GameInfoToAsciiString( const GameInfo *game ) AsciiString str; if (slot && slot->isHuman()) { - AsciiString tmp; //all this data goes after name - tmp.format( ",%X,%d,%c%c,%d,%d,%d,%d,%d:", - slot->getIP(), slot->getPort(), - (slot->isAccepted()?'T':'F'), - (slot->hasMap()?'T':'F'), + str.format( "H%s,%X,%d,%c%c,%d,%d,%d,%d,%d:", + playerNames[i].str(), slot->getIP(), + slot->getPort(), (slot->isAccepted() ? 'T' : 'F'), + (slot->hasMap() ? 'T' : 'F'), slot->getColor(), slot->getPlayerTemplate(), slot->getStartPos(), slot->getTeamNumber(), - slot->getNATBehavior() ); - //make sure name doesn't cause overflow of m_lanMaxOptionsLength - int lenCur = tmp.getLength() + optionsString.getLength() + 2; //+2 for H and trailing ; - int lenRem = m_lanMaxOptionsLength - lenCur; //length remaining before overflowing - int lenMax = lenRem / (MAX_SLOTS-i); //share lenRem with all remaining slots - AsciiString name = WideCharStringToMultiByte(slot->getName().str()).c_str(); - while( name.getLength() > lenMax ) - name.removeLastChar(); //what a horrible way to truncate. I hate AsciiString. - - str.format( "H%s%s", name.str(), tmp.str() ); + slot->getNATBehavior()); } else if (slot && slot->isAI()) { @@ -984,13 +1069,37 @@ AsciiString GameInfoToAsciiString( const GameInfo *game ) } optionsString.concat(';'); - DEBUG_ASSERTCRASH(!TheLAN || (optionsString.getLength() < m_lanMaxOptionsLength), - ("WARNING: options string is longer than expected! Length is %d, but max is %d!", - optionsString.getLength(), m_lanMaxOptionsLength)); - return optionsString; } +AsciiString GameInfoToAsciiString(const GameInfo* game) +{ + if (!game) + { + return AsciiString::TheEmptyString; + } + + AsciiStringVec playerNames = BuildPlayerNames(*game); + AsciiString infoString = GameInfoToAsciiString(game, playerNames); + + // TheSuperHackers @bugfix Safely truncate the game info string by + // stripping characters off of player names if the overall length is too large. + if (TheLAN && (infoString.getLength() > m_lanMaxOptionsLength)) + { + const UnsignedInt truncateAmount = infoString.getLength() - m_lanMaxOptionsLength; + if (!TruncatePlayerNames(playerNames, truncateAmount)) + { + DEBUG_CRASH(("WARNING: options string is longer than expected! Length is %d, but max is %d. Attempted to truncate player names by %u characters, but was unsuccessful!", + infoString.getLength(), m_lanMaxOptionsLength, truncateAmount)); + return AsciiString::TheEmptyString; + } + + infoString = GameInfoToAsciiString(game, playerNames); + } + + return infoString; +} + static Int grabHexInt(const char *s) { char tmp[5] = "0xff"; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/LANAPI.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/LANAPI.cpp index 797930d68d..ce326379f5 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/LANAPI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/LANAPI.cpp @@ -832,7 +832,7 @@ void LANAPI::RequestGameStartTimer( Int seconds ) void LANAPI::RequestGameOptions( AsciiString gameOptions, Bool isPublic, UnsignedInt ip /* = 0 */ ) { - DEBUG_ASSERTCRASH(gameOptions.getLength() < m_lanMaxOptionsLength, ("Game options string is too long!")); + DEBUG_ASSERTCRASH(gameOptions.getLength() <= m_lanMaxOptionsLength, ("Game options string is too long!")); if (!m_currentGame) return; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index 466b9e2d48..99148869eb 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -195,6 +195,7 @@ void LANAPI::handleRequestGameInfo( LANMessage *msg, UnsignedInt senderIP ) AsciiString gameOpts = GameInfoToAsciiString(m_currentGame); strncpy(reply.GameInfo.options,gameOpts.str(),m_lanMaxOptionsLength); + reply.GameInfo.options[m_lanMaxOptionsLength] = 0; wcsncpy(reply.GameInfo.gameName, m_currentGame->getName().str(), g_lanGameNameLength); reply.GameInfo.gameName[g_lanGameNameLength] = 0; reply.GameInfo.inProgress = m_currentGame->isGameInProgress();