Skip to content

Commit d084b2f

Browse files
committed
fix(network): Prevent hang in crowded network lobby caused by long player names
1 parent 6cfcc7b commit d084b2f

File tree

5 files changed

+193
-22
lines changed

5 files changed

+193
-22
lines changed

Core/GameEngine/Include/Common/AsciiString.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,20 @@ class AsciiString
377377
AsciiString& operator=(const AsciiString& stringSrc); ///< the same as set()
378378
AsciiString& operator=(const char* s); ///< the same as set()
379379

380+
const Char& operator[](Int index) const
381+
{
382+
DEBUG_ASSERTCRASH(index >= 0 && index < getLength(), ("bad index in AsciiString::operator[]"));
383+
return peek()[index];
384+
}
385+
386+
Char& operator[](Int index)
387+
{
388+
Int length = getLength();
389+
DEBUG_ASSERTCRASH(index >= 0 && index < length, ("bad index in AsciiString::operator[]"));
390+
ensureUniqueBufferOfSize(length + 1, true, NULL, NULL);
391+
return peek()[index];
392+
}
393+
380394
void debugIgnoreLeaks();
381395

382396
};

Core/GameEngine/Include/Common/UnicodeString.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,20 @@ class UnicodeString
328328

329329
UnicodeString& operator=(const UnicodeString& stringSrc); ///< the same as set()
330330
UnicodeString& operator=(const WideChar* s); ///< the same as set()
331+
332+
const WideChar& operator[](Int index) const
333+
{
334+
DEBUG_ASSERTCRASH(index >= 0 && index < getLength(), ("bad index in UnicodeString::operator[]"));
335+
return peek()[index];
336+
}
337+
338+
WideChar& operator[](Int index)
339+
{
340+
Int length = getLength();
341+
DEBUG_ASSERTCRASH(index >= 0 && index < length, ("bad index in UnicodeString::operator[]"));
342+
ensureUniqueBufferOfSize(length + 1, true, NULL, NULL);
343+
return peek()[index];
344+
}
331345
};
332346

333347

Core/GameEngine/Include/GameNetwork/LANAPI.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ struct LANMessage
406406
{
407407
char options[m_lanMaxOptionsLength+1];
408408
} GameOptions;
409+
static_assert(ARRAY_SIZE(GameOptions.options) > m_lanMaxOptionsLength, "GameOptions.options buffer must be larger than m_lanMaxOptionsLength");
409410

410411
};
411412
};

Core/GameEngine/Source/GameNetwork/GameInfo.cpp

Lines changed: 163 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
#include "GameNetwork/LANAPI.h" // for testing packet size
4545
#include "GameNetwork/LANAPICallbacks.h" // for testing packet size
4646
#include "strtok_r.h"
47+
#include <algorithm>
48+
#include <numeric>
49+
#include <utility>
4750

4851

4952

@@ -892,7 +895,130 @@ Bool GameInfo::isSandbox(void)
892895

893896
static const char slotListID = 'S';
894897

895-
AsciiString GameInfoToAsciiString( const GameInfo *game )
898+
struct LengthIndexPair
899+
{
900+
Int Length;
901+
size_t Index;
902+
friend bool operator<(const LengthIndexPair& lhs, const LengthIndexPair& rhs)
903+
{
904+
if (lhs.Length == rhs.Length)
905+
return lhs.Index < rhs.Index;
906+
return lhs.Length < rhs.Length;
907+
}
908+
friend bool operator>(const LengthIndexPair& lhs, const LengthIndexPair& rhs) { return rhs < lhs; }
909+
friend bool operator<=(const LengthIndexPair& lhs, const LengthIndexPair& rhs) { return !(lhs > rhs); }
910+
friend bool operator>=(const LengthIndexPair& lhs, const LengthIndexPair& rhs) { return !(lhs < rhs); }
911+
};
912+
913+
static AsciiStringVec BuildPlayerNames(const GameInfo& game)
914+
{
915+
AsciiStringVec playerNames;
916+
playerNames.resize(MAX_SLOTS);
917+
918+
for (Int i = 0; i < MAX_SLOTS; ++i)
919+
{
920+
const GameSlot* slot = game.getConstSlot(i);
921+
if (slot->isHuman())
922+
{
923+
playerNames[i] = WideCharStringToMultiByte(slot->getName().str()).c_str();
924+
}
925+
}
926+
927+
return playerNames;
928+
}
929+
930+
static Int SumLength(Int sum, const LengthIndexPair& l)
931+
{
932+
return sum + l.Length;
933+
}
934+
935+
static Bool TruncatePlayerNames(AsciiStringVec& playerNames, Int truncateAmount)
936+
{
937+
// wont truncate any name to below this length
938+
constexpr const Int MinimumNameLength = 1;
939+
940+
// make length+index pairs for the player names
941+
std::vector<LengthIndexPair> lengthIndex;
942+
lengthIndex.resize(playerNames.size());
943+
for (size_t pi = 0; pi < playerNames.size(); ++pi)
944+
{
945+
Int playerNameLength = playerNames[pi].getLength();
946+
lengthIndex[pi].Length = std::max(0, playerNameLength - MinimumNameLength);
947+
lengthIndex[pi].Index = pi;
948+
}
949+
950+
Int remainingNamesLength = std::accumulate(lengthIndex.begin(), lengthIndex.end(), 0, SumLength);
951+
952+
if (truncateAmount > remainingNamesLength)
953+
{
954+
DEBUG_LOG(("TruncatePlayerNames - Requested to truncate %u chars from player names, but only %u were available for truncation.",
955+
truncateAmount, remainingNamesLength));
956+
return false;
957+
}
958+
959+
// sort based on length in ascending order
960+
std::sort(lengthIndex.begin(), lengthIndex.end());
961+
962+
Int truncateNameAmount = 0;
963+
for (size_t i = 0; i < lengthIndex.size(); ++i)
964+
{
965+
// round avg name length up, which will penalize the final entry (longest name) as it will have to account for the roundings
966+
Int avgNameLength = ((remainingNamesLength - truncateAmount) + (playerNames.size() - i - 1)) / (playerNames.size() - i);
967+
remainingNamesLength -= lengthIndex[i].Length;
968+
if (lengthIndex[i].Length <= avgNameLength)
969+
{
970+
continue;
971+
}
972+
973+
// ensure a longer name is not truncated less than a previous, shorter name
974+
truncateNameAmount = std::max(truncateNameAmount, lengthIndex[i].Length - avgNameLength);
975+
if (i == lengthIndex.size() - 1)
976+
{
977+
// ensure we account for rounding errors when truncating the last, longest entry
978+
truncateNameAmount = std::max(truncateAmount, truncateNameAmount);
979+
}
980+
981+
// as the name is UTF-8, make sure we don't truncate part of a multibyte character
982+
while (lengthIndex[i].Length - truncateNameAmount >= MinimumNameLength
983+
&& (playerNames[lengthIndex[i].Index][lengthIndex[i].Length - truncateNameAmount + 1] & 0xC0) == 0x80)
984+
{
985+
++truncateNameAmount; // move back to the start of the multibyte character
986+
}
987+
988+
playerNames[lengthIndex[i].Index].truncateBy(truncateNameAmount);
989+
truncateAmount -= truncateNameAmount;
990+
}
991+
992+
return true;
993+
}
994+
995+
static void EnsureUniqueNames(AsciiStringVec& playerNames)
996+
{
997+
// ensure there are no duplicates in the list of player names
998+
std::set<AsciiString> uniqueNames;
999+
for (size_t i = 0; i < playerNames.size(); ++i)
1000+
{
1001+
static_assert(MAX_SLOTS < 10, "Name collision avoidance assumes less than 10 players in the game.");
1002+
AsciiString& playerName = playerNames[i];
1003+
1004+
if (playerName.isEmpty())
1005+
continue;
1006+
1007+
Int charOffset = -1;
1008+
Int nameLength = playerName.getLength();
1009+
while (uniqueNames.insert(playerName).second == false)
1010+
{
1011+
// The name already exists, so change the last char to the number index of the player in the game.
1012+
// If that fails to be unique, iterate through 0-9 and change the last char to ensure differentiation.
1013+
// Guaranteed to find a unique name as the number of slots is less than 10.
1014+
char charToTry = '0' + static_cast<char>(charOffset == -1 ? i : charOffset);
1015+
playerName[nameLength - 1] = charToTry;
1016+
++charOffset;
1017+
}
1018+
}
1019+
}
1020+
1021+
AsciiString GameInfoToAsciiString(const GameInfo *game, const AsciiStringVec& playerNames)
8961022
{
8971023
if (!game)
8981024
return AsciiString::TheEmptyString;
@@ -918,7 +1044,7 @@ AsciiString GameInfoToAsciiString( const GameInfo *game )
9181044
newMapName.concat(token);
9191045
mapName.nextToken(&token, "\\/");
9201046
}
921-
DEBUG_LOG(("Map name is %s", mapName.str()));
1047+
DEBUG_LOG(("Map name is %s", newMapName.str()));
9221048
}
9231049

9241050
AsciiString optionsString;
@@ -941,23 +1067,13 @@ AsciiString GameInfoToAsciiString( const GameInfo *game )
9411067
AsciiString str;
9421068
if (slot && slot->isHuman())
9431069
{
944-
AsciiString tmp; //all this data goes after name
945-
tmp.format( ",%X,%d,%c%c,%d,%d,%d,%d,%d:",
946-
slot->getIP(), slot->getPort(),
947-
(slot->isAccepted()?'T':'F'),
948-
(slot->hasMap()?'T':'F'),
1070+
str.format( "H%s,%X,%d,%c%c,%d,%d,%d,%d,%d:",
1071+
playerNames[i].str(), slot->getIP(),
1072+
slot->getPort(), (slot->isAccepted() ? 'T' : 'F'),
1073+
(slot->hasMap() ? 'T' : 'F'),
9491074
slot->getColor(), slot->getPlayerTemplate(),
9501075
slot->getStartPos(), slot->getTeamNumber(),
951-
slot->getNATBehavior() );
952-
//make sure name doesn't cause overflow of m_lanMaxOptionsLength
953-
int lenCur = tmp.getLength() + optionsString.getLength() + 2; //+2 for H and trailing ;
954-
int lenRem = m_lanMaxOptionsLength - lenCur; //length remaining before overflowing
955-
int lenMax = lenRem / (MAX_SLOTS-i); //share lenRem with all remaining slots
956-
AsciiString name = WideCharStringToMultiByte(slot->getName().str()).c_str();
957-
while( name.getLength() > lenMax )
958-
name.removeLastChar(); //what a horrible way to truncate. I hate AsciiString.
959-
960-
str.format( "H%s%s", name.str(), tmp.str() );
1076+
slot->getNATBehavior());
9611077
}
9621078
else if (slot && slot->isAI())
9631079
{
@@ -989,13 +1105,39 @@ AsciiString GameInfoToAsciiString( const GameInfo *game )
9891105
}
9901106
optionsString.concat(';');
9911107

992-
DEBUG_ASSERTCRASH(!TheLAN || (optionsString.getLength() < m_lanMaxOptionsLength),
993-
("WARNING: options string is longer than expected! Length is %d, but max is %d!",
994-
optionsString.getLength(), m_lanMaxOptionsLength));
995-
9961108
return optionsString;
9971109
}
9981110

1111+
AsciiString GameInfoToAsciiString(const GameInfo* game)
1112+
{
1113+
if (!game)
1114+
{
1115+
return AsciiString::TheEmptyString;
1116+
}
1117+
1118+
AsciiStringVec playerNames = BuildPlayerNames(*game);
1119+
AsciiString infoString = GameInfoToAsciiString(game, playerNames);
1120+
1121+
// TheSuperHackers @bugfix Safely truncate the game info string by
1122+
// stripping characters off of player names if the overall length is too large.
1123+
if (TheLAN && (infoString.getLength() > m_lanMaxOptionsLength))
1124+
{
1125+
const UnsignedInt truncateAmount = infoString.getLength() - m_lanMaxOptionsLength;
1126+
if (!TruncatePlayerNames(playerNames, truncateAmount))
1127+
{
1128+
DEBUG_CRASH(("WARNING: options string is longer than expected! Length is %d, but max is %d. "
1129+
"Attempted to truncate player names by %u characters, but was unsuccessful!",
1130+
infoString.getLength(), m_lanMaxOptionsLength, truncateAmount));
1131+
return AsciiString::TheEmptyString;
1132+
}
1133+
1134+
EnsureUniqueNames(playerNames);
1135+
infoString = GameInfoToAsciiString(game, playerNames);
1136+
}
1137+
1138+
return infoString;
1139+
}
1140+
9991141
static Int grabHexInt(const char *s)
10001142
{
10011143
char tmp[5] = "0xff";

Core/GameEngine/Source/GameNetwork/LANAPI.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,7 @@ void LANAPI::RequestGameStartTimer( Int seconds )
831831

832832
void LANAPI::RequestGameOptions( AsciiString gameOptions, Bool isPublic, UnsignedInt ip /* = 0 */ )
833833
{
834-
DEBUG_ASSERTCRASH(gameOptions.getLength() < m_lanMaxOptionsLength, ("Game options string is too long!"));
834+
DEBUG_ASSERTCRASH(gameOptions.getLength() <= m_lanMaxOptionsLength, ("Game options string is too long!"));
835835

836836
if (!m_currentGame)
837837
return;

0 commit comments

Comments
 (0)