From 61ce9f6c5e8e431700d671ecbe95c4e31d389df4 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 10 Aug 2026 12:18:14 -0400 Subject: [PATCH 1/2] bugfix(network): Prevent LAN lobby hang with long player names --- Core/GameEngine/Include/GameNetwork/LANAPI.h | 1 + .../Source/GameNetwork/GameInfo.cpp | 35 +++++++++++++++++-- Core/GameEngine/Source/GameNetwork/LANAPI.cpp | 2 +- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/Core/GameEngine/Include/GameNetwork/LANAPI.h b/Core/GameEngine/Include/GameNetwork/LANAPI.h index a0365be185a..483540ade6b 100644 --- a/Core/GameEngine/Include/GameNetwork/LANAPI.h +++ b/Core/GameEngine/Include/GameNetwork/LANAPI.h @@ -262,6 +262,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"); }; }; diff --git a/Core/GameEngine/Source/GameNetwork/GameInfo.cpp b/Core/GameEngine/Source/GameNetwork/GameInfo.cpp index 5b05e9eb369..ca6fc99acfc 100644 --- a/Core/GameEngine/Source/GameNetwork/GameInfo.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameInfo.cpp @@ -891,6 +891,36 @@ Bool GameInfo::isSandbox() static const char slotListID = 'S'; +static Bool isUtf8ContinuationByte(Char c) +{ + return (static_cast(c) & 0xC0) == 0x80; +} + +// TheSuperHackers @bugfix Truncates the name to at most maxByteCount bytes without splitting +// a multibyte UTF-8 character. A non-positive budget empties the name; retail spun forever +// there, because removing the last character of an already empty string is a no-op. +static void truncatePlayerName(AsciiString& name, Int maxByteCount) +{ + if (maxByteCount <= 0) + { + name.clear(); + return; + } + + if (name.getLength() <= maxByteCount) + { + return; + } + + Int truncatedLength = maxByteCount; + while (truncatedLength > 0 && isUtf8ContinuationByte(name.getCharAt(truncatedLength))) + { + --truncatedLength; + } + + name.truncateTo(truncatedLength); +} + AsciiString GameInfoToAsciiString( const GameInfo *game ) { if (!game) @@ -953,8 +983,7 @@ AsciiString GameInfoToAsciiString( const GameInfo *game ) 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. + truncatePlayerName( name, lenMax ); str.format( "H%s%s", name.str(), tmp.str() ); } @@ -988,7 +1017,7 @@ AsciiString GameInfoToAsciiString( const GameInfo *game ) } optionsString.concat(';'); - DEBUG_ASSERTCRASH(!TheLAN || (optionsString.getLength() < m_lanMaxOptionsLength), + 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)); diff --git a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp index 8cbfbdea6c5..a5c8793e6aa 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp @@ -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; From 3497569ced2750a875d5c9a68715572456154539 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Tue, 11 Aug 2026 17:30:34 -0400 Subject: [PATCH 2/2] refactor(utf8): Move the UTF-8 truncation rule into WWLib --- .../Source/GameNetwork/GameInfo.cpp | 19 +-------- .../Source/WWVegas/WWLib/CMakeLists.txt | 2 + Core/Libraries/Source/WWVegas/WWLib/utf8.cpp | 41 +++++++++++++++++++ Core/Libraries/Source/WWVegas/WWLib/utf8.h | 27 ++++++++++++ 4 files changed, 72 insertions(+), 17 deletions(-) create mode 100644 Core/Libraries/Source/WWVegas/WWLib/utf8.cpp create mode 100644 Core/Libraries/Source/WWVegas/WWLib/utf8.h diff --git a/Core/GameEngine/Source/GameNetwork/GameInfo.cpp b/Core/GameEngine/Source/GameNetwork/GameInfo.cpp index ca6fc99acfc..dbe4e6073d0 100644 --- a/Core/GameEngine/Source/GameNetwork/GameInfo.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameInfo.cpp @@ -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,11 +892,6 @@ Bool GameInfo::isSandbox() static const char slotListID = 'S'; -static Bool isUtf8ContinuationByte(Char c) -{ - return (static_cast(c) & 0xC0) == 0x80; -} - // TheSuperHackers @bugfix Truncates the name to at most maxByteCount bytes without splitting // a multibyte UTF-8 character. A non-positive budget empties the name; retail spun forever // there, because removing the last character of an already empty string is a no-op. @@ -907,18 +903,7 @@ static void truncatePlayerName(AsciiString& name, Int maxByteCount) return; } - if (name.getLength() <= maxByteCount) - { - return; - } - - Int truncatedLength = maxByteCount; - while (truncatedLength > 0 && isUtf8ContinuationByte(name.getCharAt(truncatedLength))) - { - --truncatedLength; - } - - name.truncateTo(truncatedLength); + name.truncateTo(static_cast(Utf8_Truncate_Len(name.str(), name.getLength(), maxByteCount))); } AsciiString GameInfoToAsciiString( const GameInfo *game ) diff --git a/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt b/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt index 4aef55e2082..77721250c6c 100644 --- a/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt +++ b/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt @@ -134,6 +134,8 @@ set(WWLIB_SRC trim.cpp trim.h uarray.h + utf8.cpp + utf8.h vector.cpp Vector.h visualc.h diff --git a/Core/Libraries/Source/WWVegas/WWLib/utf8.cpp b/Core/Libraries/Source/WWVegas/WWLib/utf8.cpp new file mode 100644 index 00000000000..0f8638361ee --- /dev/null +++ b/Core/Libraries/Source/WWVegas/WWLib/utf8.cpp @@ -0,0 +1,41 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "always.h" +#include "utf8.h" + +// 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; +} diff --git a/Core/Libraries/Source/WWVegas/WWLib/utf8.h b/Core/Libraries/Source/WWVegas/WWLib/utf8.h new file mode 100644 index 00000000000..ae5758b9478 --- /dev/null +++ b/Core/Libraries/Source/WWVegas/WWLib/utf8.h @@ -0,0 +1,27 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +#include + +// 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);