-
Notifications
You must be signed in to change notification settings - Fork 79
[ZH] Prevent hang in network lobby with long player names #1119
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
5efb9b8
ca773e0
b7eafeb
d5a4150
1ae9876
cdbf4b7
aab7f02
f8600c6
ca15e22
60d1b56
c83bae0
87b552e
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,8 @@ | ||||||||||
#include "GameNetwork/LANAPI.h" // for testing packet size | |||||||||||
#include "GameNetwork/LANAPICallbacks.h" // for testing packet size | |||||||||||
#include "strtok_r.h" | |||||||||||
#include <algorithm> | |||||||||||
#include <utility> | |||||||||||
|
|||||||||||
#ifdef RTS_INTERNAL | |||||||||||
// for occasional debugging... | |||||||||||
|
@@ -899,7 +901,97 @@ Bool GameInfo::isSandbox(void) | ||||||||||
|
|||||||||||
static const char slotListID = 'S'; | |||||||||||
|
|||||||||||
AsciiString GameInfoToAsciiString( const GameInfo *game ) | |||||||||||
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<LengthIndexPair> 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.\n", truncateAmount, availableForTruncation)); | |||||||||||
return false; | |||||||||||
} | |||||||||||
|
|||||||||||
// sort based on length in descending order | |||||||||||
std::sort(lengthIndex.begin(), lengthIndex.end(), std::greater<LengthIndexPair>()); | |||||||||||
|
|||||||||||
// determine how long each of the player names should be | |||||||||||
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. This loop does not yet look optimal. It goes one char at a time, over all players. It looks like it should be possible to skip ahead multiple characters by peeking into the next player name (if it is not the last one).
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. I was a bit afraid of making it too complicated again, but the latest iteration will do greater steps in the target length. It's a bit more complex, but perhaps manageable? 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. In inquired with Chat GPT about it. Can you check? Chat GPTGreat! To improve performance, we can avoid repeated scanning and dynamic target recomputation by switching to a leveling algorithm, inspired by how you might "flatten terrain" from the top down. ✅ Optimal Fair Truncation Algorithm — O(n log n)📌 Key Ideas:
This avoids the repeated loop-reset logic of your original code. 🔧 Optimized Implementation#include <vector>
#include <string>
#include <algorithm>
#include <cassert>
#include <iostream>
using namespace std;
// Alias types for clarity
using AsciiStringVec = std::vector<std::string>;
using Int = int;
using UnsignedInt = unsigned int;
using Bool = bool;
struct LengthIndexPair {
Int Length;
size_t Index;
};
static Bool TruncatePlayerNames(AsciiStringVec& playerNames, UnsignedInt truncateAmount) {
constexpr Int MinimumNameLength = 2;
const size_t n = playerNames.size();
if (n == 0 || truncateAmount == 0) return true;
// Prepare length/index pairs and calculate available truncation budget
vector<LengthIndexPair> entries(n);
UnsignedInt availableForTruncation = 0;
for (size_t i = 0; i < n; ++i) {
Int len = static_cast<Int>(playerNames[i].size());
entries[i] = { len, i };
availableForTruncation += max(0, len - MinimumNameLength);
}
if (truncateAmount > availableForTruncation) {
cerr << "Requested to truncate more than allowed.\n";
return false;
}
// Sort by descending length
sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
return a.Length > b.Length;
});
// Process in levels
size_t i = 0;
while (i < n && truncateAmount > 0) {
Int currentLength = entries[i].Length;
// Find the next group with a smaller length
size_t j = i + 1;
while (j < n && entries[j].Length == currentLength) ++j;
// Determine target level
Int nextLength = (j < n) ? entries[j].Length : MinimumNameLength;
Int maxReduction = currentLength - max(nextLength, MinimumNameLength);
size_t groupSize = j;
UnsignedInt cost = groupSize * maxReduction;
if (cost <= truncateAmount) {
// Flatten whole group
for (size_t k = 0; k < groupSize; ++k)
entries[k].Length -= maxReduction;
truncateAmount -= cost;
i = j; // move to next group
} else {
// Partial truncation to equal target
Int cut = truncateAmount / groupSize;
Int extra = truncateAmount % groupSize;
for (size_t k = 0; k < groupSize; ++k) {
entries[k].Length -= cut;
if (extra > 0) {
entries[k].Length -= 1;
--extra;
}
}
truncateAmount = 0;
break;
}
}
// Apply truncation
for (const auto& entry : entries) {
Int newLength = max(entry.Length, MinimumNameLength);
if ((Int)playerNames[entry.Index].size() > newLength) {
playerNames[entry.Index].resize(newLength);
}
}
return true;
} ✅ Time Complexity
🧪 ExampleInput: playerNames = {"Jonathan", "Tom", "Christopher", "Al", "Dan"};
truncateAmount = 10; After truncation:
Let’s compare the performance characteristics of your original implementation and the optimized one I provided, to show exactly why the latter is more efficient. 🧠 1. Your Original Implementation🔁 Key Pattern:
❌ Performance Implications:
🔄 Example Worst-Case Behavior:If you have
That’s ⚡ 2. Optimized Implementation✅ Key Strategy:
📈 Performance Characteristics:
💡 AnalogyImagine you have towers of blocks (string lengths) and you want to remove Your Approach:
Problem: You often keep re-checking previous towers after recalculating your target — redundant work. Optimized Approach:
Benefit: You never redo work; every block removal counts. 🧪 Example Timing Difference (Theoretical)
|
|||||||||||
Int currentTargetLength = lengthIndex[0].Length - 1; | |||||||||||
while (truncateAmount > 0) | |||||||||||
{ | |||||||||||
for (size_t i = 0; i < lengthIndex.size(); ++i) | |||||||||||
{ | |||||||||||
if (lengthIndex[i].Length > currentTargetLength) | |||||||||||
{ | |||||||||||
Int truncateCurrent = std::min<Int>(truncateAmount, lengthIndex[i].Length - currentTargetLength); | |||||||||||
lengthIndex[i].Length -= truncateCurrent; | |||||||||||
truncateAmount -= truncateCurrent; | |||||||||||
} | |||||||||||
slurmlord marked this conversation as resolved.
Show resolved
Hide resolved
|
|||||||||||
|
|||||||||||
if (truncateAmount == 0) | |||||||||||
{ | |||||||||||
break; | |||||||||||
} | |||||||||||
|
|||||||||||
if (lengthIndex[i].Length < currentTargetLength) | |||||||||||
{ | |||||||||||
// set target length to either the length of position i, or the remaining amount to truncate divided across all the previous entries, rounding upwards. | |||||||||||
currentTargetLength = std::max(static_cast<UnsignedInt>(lengthIndex[i].Length), lengthIndex[0].Length - ((truncateAmount + i) / (i + 1))); | |||||||||||
// start over again with new target length | |||||||||||
i = -1; | |||||||||||
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. Maybe set i = 0 here and ++i at the end. |
|||||||||||
continue; | |||||||||||
} | |||||||||||
} | |||||||||||
|
|||||||||||
// All entries are of equal length, or we're finished. Figure out the length of all entries if truncated by the same amount, rounding upwards. | |||||||||||
currentTargetLength = lengthIndex[0].Length - ((truncateAmount + lengthIndex.size() - 1) / lengthIndex.size()); | |||||||||||
} | |||||||||||
|
|||||||||||
// truncate each name to its new length | |||||||||||
for (size_t ti = 0; ti < lengthIndex.size(); ++ti) | |||||||||||
{ | |||||||||||
int charsToRemove = playerNames[lengthIndex[ti].Index].getLength() - lengthIndex[ti].Length; | |||||||||||
if (charsToRemove > 0) | |||||||||||
{ | |||||||||||
DEBUG_LOG(("TruncatePlayerNames - truncating '%s' by %d chars to ", playerNames[lengthIndex[ti].Index].str(), charsToRemove)); | |||||||||||
playerNames[lengthIndex[ti].Index].truncate(charsToRemove); | |||||||||||
DEBUG_LOG(("'%s' (target length=%d).\n", playerNames[lengthIndex[ti].Index].str(), lengthIndex[ti].Length)); | |||||||||||
} | |||||||||||
} | |||||||||||
|
|||||||||||
return true; | |||||||||||
} | |||||||||||
|
|||||||||||
AsciiString GameInfoToAsciiString(const GameInfo *game, const AsciiStringVec& playerNames) | |||||||||||
{ | |||||||||||
if (!game) | |||||||||||
return AsciiString::TheEmptyString; | |||||||||||
|
@@ -925,7 +1017,7 @@ AsciiString GameInfoToAsciiString( const GameInfo *game ) | ||||||||||
newMapName.concat(token); | |||||||||||
mapName.nextToken(&token, "\\/"); | |||||||||||
} | |||||||||||
DEBUG_LOG(("Map name is %s\n", mapName.str())); | |||||||||||
DEBUG_LOG(("Map name is %s\n", newMapName.str())); | |||||||||||
} | |||||||||||
|
|||||||||||
AsciiString optionsString; | |||||||||||
|
@@ -943,23 +1035,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()) | |||||||||||
{ | |||||||||||
|
@@ -991,13 +1073,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!\n", | |||||||||||
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!\n", | |||||||||||
infoString.getLength(), m_lanMaxOptionsLength, truncateAmount)); | |||||||||||
return AsciiString::TheEmptyString; | |||||||||||
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. So originally it would have returned the long string if it was unable to (theoretically) truncate. What happens if we return an empty string or a string that is longer than the cap? |
|||||||||||
} | |||||||||||
|
|||||||||||
infoString = GameInfoToAsciiString(game, playerNames); | |||||||||||
} | |||||||||||
|
|||||||||||
return infoString; | |||||||||||
} | |||||||||||
|
|||||||||||
static Int grabHexInt(const char *s) | |||||||||||
{ | |||||||||||
char tmp[5] = "0xff"; | |||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better move this into the cpp if it is only used there.