-
Notifications
You must be signed in to change notification settings - Fork 256
bugfix(network): Prevent LAN lobby hang with long player names #3039
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
79e8b4d
2a1b69e
ff1e7af
7699fa3
865c66e
654d854
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,7 @@ | |
| #include "GameNetwork/LANAPI.h" // for testing packet size | ||
| #include "GameNetwork/LANAPICallbacks.h" // for testing packet size | ||
| #include "WWLib/strtok_r.h" | ||
| #include "WWLib/utf8.h" | ||
|
|
||
|
|
||
|
|
||
|
|
@@ -891,12 +892,75 @@ Bool GameInfo::isSandbox() | |
|
|
||
| static const char slotListID = 'S'; | ||
|
|
||
| AsciiString GameInfoToAsciiString( const GameInfo *game ) | ||
| // Shorten player names without cutting a UTF-8 character in half. | ||
| static void truncatePlayerNameToByteCount(AsciiString& name, Int maxByteCount) | ||
| { | ||
| if (!game) | ||
| return AsciiString::TheEmptyString; | ||
| const size_t truncatedLength = Utf8_Truncate_Len(name.str(), name.getLength(), maxByteCount); | ||
| name.truncateTo(static_cast<Int>(truncatedLength)); | ||
| } | ||
|
|
||
| static Int getMinPlayerNameLength(const AsciiString& name) | ||
| { | ||
| for (Int maxByteCount = 1; maxByteCount <= name.getLength(); ++maxByteCount) | ||
| { | ||
| const size_t truncatedLength = Utf8_Truncate_Len(name.str(), name.getLength(), maxByteCount); | ||
| if (truncatedLength > 0) | ||
| { | ||
| return static_cast<Int>(truncatedLength); | ||
| } | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| static Bool truncatePlayerNames(const GameInfo& game, AsciiString playerNames[MAX_SLOTS], Int maxPlayerNamesLength) | ||
| { | ||
| Int minLengths[MAX_SLOTS] = { 0 }; | ||
| Int minTotalLength = 0; | ||
| Int playerCount = 0; | ||
| Int i; | ||
|
|
||
| for (i = 0; i < MAX_SLOTS; ++i) | ||
| { | ||
| const GameSlot *slot = game.getConstSlot(i); | ||
| if (slot && slot->isHuman()) | ||
| { | ||
| minLengths[i] = getMinPlayerNameLength(playerNames[i]); | ||
| if (minLengths[i] == 0) | ||
| { | ||
| // Every serialized human must retain at least one complete UTF-8 character. | ||
| return false; | ||
| } | ||
| minTotalLength += minLengths[i]; | ||
| ++playerCount; | ||
| } | ||
| } | ||
|
|
||
| if (playerCount == 0 || maxPlayerNamesLength < minTotalLength) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| Int remainingLength = maxPlayerNamesLength; | ||
| for (i = 0; i < MAX_SLOTS; ++i) | ||
| { | ||
| const GameSlot *slot = game.getConstSlot(i); | ||
| if (slot && slot->isHuman()) | ||
| { | ||
| const Int extraLength = (remainingLength - minTotalLength) / playerCount; | ||
| truncatePlayerNameToByteCount(playerNames[i], minLengths[i] + extraLength); | ||
| remainingLength -= playerNames[i].getLength(); | ||
| minTotalLength -= minLengths[i]; | ||
| --playerCount; | ||
| } | ||
| } | ||
|
|
||
| AsciiString mapName = game->getMap(); | ||
| return true; | ||
| } | ||
|
|
||
| static AsciiString buildGameInfoAsciiString(const GameInfo& game, const AsciiString playerNames[MAX_SLOTS]) | ||
| { | ||
| AsciiString mapName = game.getMap(); | ||
| mapName = TheGameState->realMapPathToPortableMapPath(mapName); | ||
| AsciiString newMapName; | ||
| if (!mapName.isEmpty()) | ||
|
|
@@ -922,20 +986,20 @@ AsciiString GameInfoToAsciiString( const GameInfo *game ) | |
|
|
||
| AsciiString optionsString; | ||
| #if RTS_GENERALS | ||
| optionsString.format("M=%2.2x%s;MC=%X;MS=%d;SD=%d;C=%d;", game->getMapContentsMask(), newMapName.str(), | ||
| game->getMapCRC(), game->getMapSize(), game->getSeed(), game->getCRCInterval()); | ||
| optionsString.format("M=%2.2x%s;MC=%X;MS=%d;SD=%d;C=%d;", game.getMapContentsMask(), newMapName.str(), | ||
| game.getMapCRC(), game.getMapSize(), game.getSeed(), game.getCRCInterval()); | ||
| #else | ||
| optionsString.format("US=%d;M=%2.2x%s;MC=%X;MS=%d;SD=%d;C=%d;SR=%u;SC=%u;O=%c;", game->getUseStats(), game->getMapContentsMask(), newMapName.str(), | ||
| game->getMapCRC(), game->getMapSize(), game->getSeed(), game->getCRCInterval(), game->getSuperweaponRestriction(), | ||
| game->getStartingCash().countMoney(), game->oldFactionsOnly() ? 'Y' : 'N' ); | ||
| optionsString.format("US=%d;M=%2.2x%s;MC=%X;MS=%d;SD=%d;C=%d;SR=%u;SC=%u;O=%c;", game.getUseStats(), game.getMapContentsMask(), newMapName.str(), | ||
| game.getMapCRC(), game.getMapSize(), game.getSeed(), game.getCRCInterval(), game.getSuperweaponRestriction(), | ||
| game.getStartingCash().countMoney(), game.oldFactionsOnly() ? 'Y' : 'N' ); | ||
| #endif | ||
|
|
||
| //add player info for each slot | ||
| optionsString.concat(slotListID); | ||
| optionsString.concat('='); | ||
| for (Int i=0; i<MAX_SLOTS; ++i) | ||
| { | ||
| const GameSlot *slot = game->getConstSlot(i); | ||
| const GameSlot *slot = game.getConstSlot(i); | ||
|
|
||
| AsciiString str; | ||
| if (slot && slot->isHuman()) | ||
|
|
@@ -948,15 +1012,8 @@ AsciiString GameInfoToAsciiString( const GameInfo *game ) | |
| 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() ); | ||
|
|
||
| str.format( "H%s%s", playerNames[i].str(), tmp.str() ); | ||
| } | ||
| else if (slot && slot->isAI()) | ||
| { | ||
|
|
@@ -988,9 +1045,49 @@ 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; | ||
| } | ||
|
|
||
| AsciiString playerNames[MAX_SLOTS]; | ||
| Int playerNamesLength = 0; | ||
| for (Int i = 0; i < MAX_SLOTS; ++i) | ||
| { | ||
| const GameSlot *slot = game->getConstSlot(i); | ||
| if (slot && slot->isHuman()) | ||
| { | ||
| playerNames[i] = WideCharStringToMultiByte(slot->getName().str()).c_str(); | ||
| playerNamesLength += playerNames[i].getLength(); | ||
| } | ||
| } | ||
|
|
||
| // TheSuperHackers @bugfix bobtista 23/08/2026 Prevent an infinite loop when player names exceed | ||
| // the LAN options limit by rebuilding the payload with bounded UTF-8 names. | ||
| AsciiString optionsString = buildGameInfoAsciiString(*game, playerNames); | ||
| Bool optionsFit = TheLAN == nullptr || optionsString.getLength() <= m_lanMaxOptionsLength; | ||
| if (!optionsFit) | ||
| { | ||
| const Int fixedLength = optionsString.getLength() - playerNamesLength; | ||
| const Int maxPlayerNamesLength = m_lanMaxOptionsLength - fixedLength; | ||
| if (truncatePlayerNames(*game, playerNames, maxPlayerNamesLength)) | ||
| { | ||
| optionsString = buildGameInfoAsciiString(*game, playerNames); | ||
| optionsFit = optionsString.getLength() <= m_lanMaxOptionsLength; | ||
| } | ||
| } | ||
|
|
||
| if (!optionsFit) | ||
| { | ||
| DEBUG_CRASH(("WARNING: options string cannot fit within the expected length! Length is %d, but max is %d!", | ||
| optionsString.getLength(), m_lanMaxOptionsLength)); | ||
| return AsciiString::TheEmptyString; | ||
| } | ||
|
Comment on lines
+1067
to
+1090
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '870,1110p' Core/GameEngine/Source/GameNetwork/GameInfo.cpp
rg -n 'GameInfoToAsciiString|RequestGameOptions|GameOptions|options\[' Core/GameEngine/Source/GameNetwork Core/GameEngine/Include/GameNetwork
sed -n '240,280p' Core/GameEngine/Include/GameNetwork/LANAPI.h
sed -n '800,860p' Core/GameEngine/Source/GameNetwork/LANAPI.cppRepository: TheSuperHackers/GeneralsGameCode Length of output: 17398 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- LANGameInfo.cpp ---'
sed -n '230,315p' Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp
printf '%s\n' '--- LANAPI.cpp publication path ---'
sed -n '690,735p' Core/GameEngine/Source/GameNetwork/LANAPI.cpp
sed -n '825,875p' Core/GameEngine/Source/GameNetwork/LANAPI.cpp
printf '%s\n' '--- LANAPIhandlers.cpp relevant callers/consumer ---'
sed -n '45,160p' Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp
sed -n '180,210p' Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp
sed -n '420,455p' Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp
sed -n '515,545p' Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp
sed -n '660,690p' Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp
sed -n '710,730p' Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp
printf '%s\n' '--- LANAPICallbacks.cpp relevant callers ---'
sed -n '110,140p' Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp
sed -n '450,475p' Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp
sed -n '500,540p' Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp
sed -n '580,600p' Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cppRepository: TheSuperHackers/GeneralsGameCode Length of output: 15987 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- GameInfo parser ---'
sed -n '1090,1325p' Core/GameEngine/Source/GameNetwork/GameInfo.cpp
printf '%s\n' '--- LANGameInfo parser return ---'
sed -n '245,330p' Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp
printf '%s\n' '--- all direct publication call sites ---'
rg -n -C 4 'GameInfoToAsciiString\\(|GenerateGameOptionsString\\(' Core/GameEngine/Source/GameNetworkRepository: TheSuperHackers/GeneralsGameCode Length of output: 9908 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- parser return and required fields ---'
rg -n -C 8 'sawMap|sawSlotlist|optionsOk|return ' Core/GameEngine/Source/GameNetwork/GameInfo.cpp | tail -n 180
printf '%s\n' '--- direct serialization callers ---'
rg -n -F -C 3 'GameInfoToAsciiString(' Core/GameEngine/Source/GameNetwork
printf '%s\n' '--- generated-options callers ---'
rg -n -F -C 3 'GenerateGameOptionsString(' Core/GameEngine/Source/GameNetworkRepository: TheSuperHackers/GeneralsGameCode Length of output: 18314 Reject empty game-options payloads before LAN publication. When |
||
|
|
||
| return optionsString; | ||
| } | ||
|
|
@@ -1645,4 +1742,3 @@ void SkirmishGameInfo::loadPostProcess() | |
| { | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.