bugfix(network): Prevent LAN lobby hang with long player names - #3039
bugfix(network): Prevent LAN lobby hang with long player names#3039bobtista wants to merge 5 commits into
Conversation
|
| 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
Reviews (1): Last reviewed commit: "bugfix(network): Prevent LAN lobby hang ..." | Re-trigger Greptile
Skyaero42
left a comment
There was a problem hiding this comment.
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.
| Int remainingTruncatableByteCount = 0; | ||
|
|
||
| // Build truncatable byte count and player index pairs for the player names. | ||
| for (Int pi = 0; pi < MAX_SLOTS; ++pi) |
There was a problem hiding this comment.
pi could be confused with the number pi. Maybe just use i?
| } | ||
| } | ||
|
|
||
| static Bool IsUtf8ContinuationByte(Char c) |
There was a problem hiding this comment.
This should be an Asciistring function. It does not belong in GameInfo
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
A separate encoding class would definitely have my favour.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
7900833 to
85030a1
Compare
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. |
85030a1 to
61ce9f6
Compare
3497569 to
2f84d0f
Compare
| 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 ); |
There was a problem hiding this comment.
This change looks different from the original one by Slurmlord. Why?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
But Slurmlord already implemented all this logic, why not use it then?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I just pushed this and updated the description
xezon
left a comment
There was a problem hiding this comment.
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); |
| return true; | ||
| } | ||
|
|
||
| static AsciiString buildGameInfoAsciiString(const GameInfo *game, const AsciiString playerNames[]) |
There was a problem hiding this comment.
Maybe array size should specify MAX_SLOTS ?
There was a problem hiding this comment.
done, and on both truncatePlayerNames and buildGameInfoAsciiString
d0a25c9 to
a4e99ac
Compare
WWLib/utf8.h, as does feat(string): Implement UTF-8 string conversion and validation functions #2528; whichever lands second can rebaseGameInfoToAsciiStringserializes the LAN lobby state into a string with a 400-byte limit. The existing code truncates each player name while appending its slot:Once the fixed portion of the options string consumes the remaining budget,
lenMaxbecomes negative. The loop removes the entire name, after whichAsciiString::removeLastCharbecomes a no-op. Because0 > lenMaxremains 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:
A final guard rejects any result that remains oversized. Therefore every non-empty LAN options payload returned by
GameInfoToAsciiStringis at most 400 bytes.Truncation cuts only at UTF-8 character boundaries through
Utf8_Truncate_LeninWWLib/utf8.h, keeping encoding rules outsideGameInfo. Supporting changes add a compile-time check that theGameOptions.optionsbuffer exceedsm_lanMaxOptionsLengthand 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.cppandutf8.cpppass targeted syntax compilation against the macOS integration stack.Follow-up: cache each converted player name in
GameSlotso it is not rebuilt on every room refresh, as suggested in #1119.Todo:
Core