Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Core/GameEngine/Include/GameNetwork/LANAPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ struct LANMessage
{
char options[m_lanMaxOptionsLength+1];
} GameOptions;
static_assert(ARRAY_SIZE(GameOptions.options) > m_lanMaxOptionsLength, "GameOptions.options buffer must be larger than m_lanMaxOptionsLength");

};
};
Expand Down
136 changes: 120 additions & 16 deletions Core/GameEngine/Source/GameNetwork/GameInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"



Expand Down Expand Up @@ -891,11 +892,80 @@ Bool GameInfo::isSandbox()

static const char slotListID = 'S';

AsciiString GameInfoToAsciiString( const GameInfo *game )
// TheSuperHackers @bugfix bobtista 23/08/2026 Truncate player names without splitting a
// multibyte UTF-8 character. A non-positive budget empties the name instead of looping forever.
static void truncatePlayerName(AsciiString& name, Int maxByteCount)
{
if (!game)
return AsciiString::TheEmptyString;
if (maxByteCount <= 0)
{
name.clear();
return;
}

const size_t truncatedLength = Utf8_Truncate_Len(name.str(), name.getLength(), maxByteCount);
name.truncateTo(static_cast<Int>(truncatedLength));
}

static Int getMinimumPlayerNameLength(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 maxTotalLength)
{
Int minimumLengths[MAX_SLOTS] = { 0 };
Int minimumTotalLength = 0;
Int playerCount = 0;
Int i;

for (i = 0; i < MAX_SLOTS; ++i)
{
const GameSlot *slot = game->getConstSlot(i);
if (slot && slot->isHuman())
{
minimumLengths[i] = getMinimumPlayerNameLength(playerNames[i]);
if (minimumLengths[i] == 0)
{
return false;
}
minimumTotalLength += minimumLengths[i];
++playerCount;
}
}

if (playerCount == 0 || maxTotalLength < minimumTotalLength)
{
return false;
}

Int remainingLength = maxTotalLength;
for (i = 0; i < MAX_SLOTS; ++i)
{
const GameSlot *slot = game->getConstSlot(i);
if (slot && slot->isHuman())
{
const Int extraLength = (remainingLength - minimumTotalLength) / playerCount;
truncatePlayerName(playerNames[i], minimumLengths[i] + extraLength);
remainingLength -= playerNames[i].getLength();
minimumTotalLength -= minimumLengths[i];
--playerCount;
}
}

return true;
}

static AsciiString buildGameInfoAsciiString(const GameInfo *game, const AsciiString playerNames[MAX_SLOTS])
{
AsciiString mapName = game->getMap();
mapName = TheGameState->realMapPathToPortableMapPath(mapName);
AsciiString newMapName;
Expand Down Expand Up @@ -948,15 +1018,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())
{
Expand Down Expand Up @@ -988,9 +1051,51 @@ 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();
}
}

AsciiString optionsString = buildGameInfoAsciiString(game, playerNames);
// TheSuperHackers @bugfix bobtista 23/08/2026 Build with full names first so the second pass
// can reserve the exact fixed-field length and divide the remaining wire budget between names.
if (TheLAN && optionsString.getLength() > m_lanMaxOptionsLength)
{
const Int fixedLength = optionsString.getLength() - playerNamesLength;
const Int maxPlayerNamesLength = m_lanMaxOptionsLength - fixedLength;
if (!truncatePlayerNames(game, playerNames, maxPlayerNamesLength))
{
DEBUG_CRASH(("WARNING: options string is longer than expected! Length is %d, but max is %d!",
optionsString.getLength(), m_lanMaxOptionsLength));
return AsciiString::TheEmptyString;
}

optionsString = buildGameInfoAsciiString(game, playerNames);
}

if (TheLAN && optionsString.getLength() > m_lanMaxOptionsLength)
{
DEBUG_CRASH(("WARNING: options string is longer than expected after truncation! Length is %d, but max is %d!",
optionsString.getLength(), m_lanMaxOptionsLength));
return AsciiString::TheEmptyString;
}

return optionsString;
}
Expand Down Expand Up @@ -1645,4 +1750,3 @@ void SkirmishGameInfo::loadPostProcess()
{
}


2 changes: 1 addition & 1 deletion Core/GameEngine/Source/GameNetwork/LANAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,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;
Expand Down
21 changes: 21 additions & 0 deletions Core/Libraries/Source/WWVegas/WWLib/utf8.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,24 @@ size_t Utf8_To_Wide(wchar_t* dest, size_t destLen, const char* src, size_t srcLe
}
return needed;
}

// A UTF-8 continuation byte matches 10xxxxxx, so it can never start a sequence.
static bool Utf8_Is_Continuation_Byte(char c)
{
return ((unsigned char)c & 0xC0) == 0x80;
}

size_t Utf8_Truncate_Len(const char* src, size_t srcLen, size_t maxLen)
{
if (srcLen <= maxLen)
{
return srcLen;
}

size_t len = maxLen;
while (len > 0 && Utf8_Is_Continuation_Byte(src[len]))
{
--len;
}
return len;
}
6 changes: 6 additions & 0 deletions Core/Libraries/Source/WWVegas/WWLib/utf8.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,9 @@ size_t Wide_To_Utf8(char* dest, size_t destLen, const wchar_t* src, size_t srcLe
// that many wide characters plus one for the terminator. Pass destLen 0 to measure without writing.
// Returns UTF8_INVALID if src is not well-formed UTF-8, setting dest[0] to L'\0' if destLen > 0.
size_t Utf8_To_Wide(wchar_t* dest, size_t destLen, const char* src, size_t srcLen);

// Returns the largest length not greater than maxLen at which the srcLen bytes of the UTF-8 string
// src can be cut without splitting a multibyte sequence, by backing off the continuation bytes at
// the cut point. Returns srcLen when the string already fits in maxLen. Returns 0 when no whole
// sequence fits, which is also what malformed UTF-8 yields once it has no lead byte to back off to.
size_t Utf8_Truncate_Len(const char* src, size_t srcLen, size_t maxLen);
Loading