Skip to content

bugfix(network): Prevent LAN lobby hang with long player names - #3039

Open
bobtista wants to merge 5 commits into
TheSuperHackers:mainfrom
bobtista:bobtista/bugfix/lan-lobby-long-name-hang
Open

bugfix(network): Prevent LAN lobby hang with long player names#3039
bobtista wants to merge 5 commits into
TheSuperHackers:mainfrom
bobtista:bobtista/bugfix/lan-lobby-long-name-hang

Conversation

@bobtista

@bobtista bobtista commented Aug 1, 2026

Copy link
Copy Markdown

GameInfoToAsciiString serializes the LAN lobby state into a string with a 400-byte limit. The existing code truncates each player name while appending its slot:

int lenRem = m_lanMaxOptionsLength - lenCur;   // can go negative
int lenMax = lenRem / (MAX_SLOTS-i);           // can go negative
while( name.getLength() > lenMax )
    name.removeLastChar();

Once the fixed portion of the options string consumes the remaining budget, lenMax becomes negative. The loop removes the entire name, after which AsciiString::removeLastChar becomes a no-op. Because 0 > lenMax remains true, the host spins forever.

The serializer now builds the complete payload with full player names first. If it exceeds 400 bytes, a second pass:

  • Calculates the exact number of bytes occupied by the fixed fields.
  • Reserves at least one complete UTF-8 character for every human player.
  • Divides the remaining name budget among the players, carrying unused space from shorter names forward.
  • Rebuilds the payload with the bounded names.
  • Returns an empty payload if the fixed fields and minimum complete names cannot fit.

A final guard rejects any result that remains oversized. Therefore every non-empty LAN options payload returned by GameInfoToAsciiString is at most 400 bytes.

Truncation cuts only at UTF-8 character boundaries through Utf8_Truncate_Len in WWLib/utf8.h, keeping encoding rules outside GameInfo. Supporting changes add a compile-time check that the GameOptions.options buffer exceeds m_lanMaxOptionsLength and allow a payload of exactly 400 bytes, which fits the 401-byte null-terminated buffer.

Truncation affects only the serialized LAN payload. The host retains the full player names, while remote clients may display their truncated forms. Names that truncate to the same prefix are left as-is; distinguishing them would require additional collision handling.

Verification for this revision:

  • GameInfo.cpp and utf8.cpp pass targeted syntax compilation against the macOS integration stack.
  • 100,000 randomized allocations using ASCII and two-, three-, and four-byte UTF-8 characters produced no oversized result or split sequence.
  • A payload of exactly 400 bytes is accepted.

Follow-up: cache each converted player name in GameSlot so it is not rebuilt on every room refresh, as suggested in #1119.

Todo:

  • A lobby of eight long names, including multibyte names, serializes without hanging
  • Every non-empty LAN options payload is at most 400 bytes
  • A payload of exactly 400 bytes is accepted
  • Replicate to Generals — N/A, the implementation is shared through Core
  • Resolve the outstanding review points inherited from [ZH] Prevent hang in network lobby with long player names #1119
  • Preserve UTF-8 validity during truncation

@bobtista bobtista self-assigned this Aug 1, 2026
@bobtista bobtista added the Bug Something is not working right, typically is user facing label Aug 1, 2026
@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown

Greptile Summary

The PR prevents LAN lobby serialization from hanging when long player names exceed the 400-byte options limit.

  • Builds the complete payload first and performs a bounded second-pass truncation only when necessary.
  • Preserves UTF-8 character boundaries and deterministically resolves names that collide after truncation.
  • Allows exactly 400 payload bytes while retaining space for the null terminator.
  • Returns an empty payload when names cannot provide enough removable bytes.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete blocking or independently actionable non-blocking issue identified.

The truncation path removes at least the required number of bytes while preserving UTF-8 boundaries and one complete character per non-empty name, and the exactly-400-byte payload safely fits the existing null-terminated buffers.

Important Files Changed

Filename Overview
Core/GameEngine/Source/GameNetwork/GameInfo.cpp Adds bounded UTF-8-aware player-name truncation, deterministic collision handling, and a second serialization pass for oversized LAN payloads.
Core/GameEngine/Source/GameNetwork/LANAPI.cpp Correctly permits a payload of exactly 400 bytes, which fits the existing 401-byte null-terminated destination.
Core/GameEngine/Include/GameNetwork/LANAPI.h Adds a compile-time assertion confirming that the LAN options buffer includes capacity beyond the payload limit.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Build UTF-8 player names] --> B[Serialize full GameInfo]
    B --> C{Payload exceeds 400 bytes?}
    C -- No --> D[Return payload]
    C -- Yes --> E[Calculate required byte reduction]
    E --> F{Enough removable name bytes?}
    F -- No --> G[Return empty string]
    F -- Yes --> H[Truncate names at UTF-8 boundaries]
    H --> I[Resolve truncated-name collisions]
    I --> J[Rebuild serialized GameInfo]
    J --> D
Loading

Reviews (1): Last reviewed commit: "bugfix(network): Prevent LAN lobby hang ..." | Re-trigger Greptile

@Skyaero42 Skyaero42 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels very complicated for what is eventually just a hack. FIxing the 400 byte gameinfo byte limit should be the true goal.

Something as simple as: count the number of available bytes for player names and divide that by the number of players - this gives the number of bytes each player name can have. Yes, it is not exact (if there a players with shorter names, that would also allow players with longer names than the threshold).

In general, there is a lot of stuff added in GameInfo that doesn't belong there. Specific byte counts of characters belong in Asciistring. Such function can probably also be generalized instead of using First and Last in functions.

Comment thread Core/GameEngine/Source/GameNetwork/GameInfo.cpp
Int remainingTruncatableByteCount = 0;

// Build truncatable byte count and player index pairs for the player names.
for (Int pi = 0; pi < MAX_SLOTS; ++pi)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pi could be confused with the number pi. Maybe just use i?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used playerIndex

Comment thread Core/GameEngine/Source/GameNetwork/GameInfo.cpp Outdated
}
}

static Bool IsUtf8ContinuationByte(Char c)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an Asciistring function. It does not belong in GameInfo

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AsciiString is the wrong home for the three UTF-8 byte helpers. They're UTF-8 encoding rules, not string operations, and putting them on AsciiString implies AsciiString knows its own encoding, which it doesn't. Probably best is WWLib/utf8.h, which is what #2528 adds. Maybe wait til after that lands?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A separate encoding class would definitely have my favour.

@bobtista bobtista Aug 11, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated just now - Utf8_Truncate_Len lives in WWLib/utf8.h now, GameInfo just keeps the empty-on-zero-budget bit since that's the actual hang fix. I didn't wait for #2528, its utf8.h is a wide<->utf8 transcoder with no truncation in it anyway, so waiting wouldn't have saved writing this. Both PRs add WWLib/utf8.h, whichever lands second can rebase.

Checked the new helper against the old loop on ascii, 2/3/4 byte sequences cut mid char and on boundaries, malformed input and zero budget. Same results.

Comment thread Core/GameEngine/Source/GameNetwork/GameInfo.cpp Outdated
Comment thread Core/GameEngine/Source/GameNetwork/GameInfo.cpp Outdated

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greptile has paused reviews on this repository — it used its 100 free open-source review credits for this billing period. Reviews resume automatically on August 26. To continue before then, an organization admin can keep reviews running past the free credits — those bill as normal usage.

@bobtista
bobtista force-pushed the bobtista/bugfix/lan-lobby-long-name-hang branch from 7900833 to 85030a1 Compare August 1, 2026 17:10
@bobtista

bobtista commented Aug 1, 2026

Copy link
Copy Markdown
Author

FIxing the 400 byte gameinfo byte limit should be the true goal.

Agreed, but retail still needs something, even if it's hacky. The 400-byte limit is part of the packed retail LAN wire layout: LANMessage is sent by size and cast directly by receivers, so enlarging the options array would change field offsets and break retail compat. Removing that limit requires a versioned or chunked protocol extension and should be a separate change. This PR keeps the existing wire format and fixes the current infinite loop; it would remain necessary as the retail-compatible fallback even after an extended protocol is introduced.

@bobtista
bobtista force-pushed the bobtista/bugfix/lan-lobby-long-name-hang branch from 85030a1 to 61ce9f6 Compare August 10, 2026 16:18
@bobtista
bobtista force-pushed the bobtista/bugfix/lan-lobby-long-name-hang branch from 3497569 to 2f84d0f Compare August 19, 2026 20:02
Comment thread Core/GameEngine/Source/GameNetwork/GameInfo.cpp Outdated
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 );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change looks different from the original one by Slurmlord. Why?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I used Slurmlord’s two-pass implementation from 1119 (truncate after serializing, with collision handling). Skyaero noted it was too complicated for what is ultimately a hack around the 400-byte limit, so I narrowed it to keep the retail per-slot budgeting and only make the truncation itself bounded and UTF-8-safe.

The hang is that lenMax can go non-positive, and removeLastChar on an empty string is a no-op, so the old loop never exits. truncatePlayerName empties the name in that case instead. The total-length bounding and collision handling from 1119 are intentionally left out. The legacy budget can still exceed 400 bytes, and bounding the total belongs with the wider fix for the limit.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But Slurmlord already implemented all this logic, why not use it then?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reused Slurmlord’s diagnosis, but not his whole implementation for correctness and scope. eg Its one-byte minimum can leave a partial UTF-8 character, and its collision pass overwrites the last byte of a name, which can also corrupt UTF-8. Supporting that pass additionally requires mutable indexing on the shared string classes.
This PR preserves the retail allocation policy and fixes the nonterminating truncation with a boundary-safe UTF-8 helper.
That said, Slurmlord’s version guarantees the final payload fits within 400 bytes, and this one does not. If we want that guarantee in this PR, I would reuse the two-pass approach but reimplement the truncation safely rather than use 1119 as-is.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just pushed this and updated the description

@xezon xezon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this new code is AI generated right? So the human reviewer would now need to check that the code was generated with a good prompt right?

for (Int maxByteCount = 1; maxByteCount <= name.getLength(); ++maxByteCount)
{
const size_t truncatedLength =
Utf8_Truncate_Len(name.str(), name.getLength(), maxByteCount);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be one line

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return true;
}

static AsciiString buildGameInfoAsciiString(const GameInfo *game, const AsciiString playerNames[])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe array size should specify MAX_SLOTS ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, and on both truncatePlayerNames and buildGameInfoAsciiString

@bobtista
bobtista force-pushed the bobtista/bugfix/lan-lobby-long-name-hang branch from d0a25c9 to a4e99ac Compare August 24, 2026 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something is not working right, typically is user facing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Network Game Room hangs if 8 players with long nicknames join

3 participants