From 1bad1b20cae91c861af634d1b753dd2ca8e4dc75 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 19 May 2026 21:48:02 +0200 Subject: [PATCH] fix(parser): restore per-user borderId read in RoomUnitParser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier "drop unsafe borderId read" fix (05ea0db) was based on the assumption that Arcturus did not append a per-user borderId at the end of each user record in RoomUsersComposer. That was true at the time — but the Infostand Borders cherry-pick on the Arcturus side (8f8f568, "feat: Infostand Borders") then added `appendInt(getInfostandBorder())` at the end of EVERY user record (single habbo, habbos collection, single bot=0, bots collection=0). With the cherry-pick applied and the parser still skipping the read, each user record left 4 unconsumed bytes on the wire. The NEXT iteration's `id = wrapper.readInt()` then picked up the previous user's borderId, the rest of the loop interpreted shifted bytes as strings/ints, and the entire roster cascaded into corruption — visible to the user as "I cannot see the other users in the room". The bytesAvailable guard around this read is intentionally NOT re-added: `bytesAvailable` is a boolean meaning "any bytes left in the whole packet?", not "any bytes left for THIS user". With Arcturus guaranteed to ship a borderId for every record (constant 0 for bots), the read must be unconditional to stay wire-aligned. --- .../parser/room/unit/RoomUnitParser.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/communication/src/messages/parser/room/unit/RoomUnitParser.ts b/packages/communication/src/messages/parser/room/unit/RoomUnitParser.ts index 8ac25a6..00ad5cd 100644 --- a/packages/communication/src/messages/parser/room/unit/RoomUnitParser.ts +++ b/packages/communication/src/messages/parser/room/unit/RoomUnitParser.ts @@ -146,16 +146,17 @@ export class RoomUnitParser implements IMessageParser user.roomEntryMethod = wrapper.readString(); user.roomEntryTeleportId = wrapper.readInt(); - // NOTE: do NOT read borderId here. `wrapper.bytesAvailable` - // is a boolean meaning "any bytes left in the whole packet?", - // not "any bytes left in THIS user". For users that aren't - // the last in the roster, bytesAvailable === true because - // the NEXT user's bytes follow — reading an int would steal - // 4 bytes from the next user and cascade-corrupt the entire - // roster (users not seeing each other on first sight). The - // border id for an individual user arrives via - // RoomUnitInfoParser (single-user packet), where the - // bytesAvailable guard is safe because there is no loop. + // Arcturus appends a trailing borderId int per user + // (RoomUsersComposer, after the Infostand Borders feature) + // for every record — habbo, bot, rentable bot — using 0 as + // the constant for the records that have no border. The + // read MUST be unconditional: a bytesAvailable guard would + // be semantically wrong here (the guard answers "any byte + // left in the whole packet?" not "any byte left for THIS + // user"), and skipping the read would leave 4 bytes per + // record and cascade-corrupt every subsequent user in the + // roster. + user.borderId = wrapper.readInt(); i++; }