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
893896static 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+
9991141static Int grabHexInt (const char *s)
10001142{
10011143 char tmp[5 ] = " 0xff" ;
0 commit comments