fix(parser): restore per-user borderId read in RoomUnitParser

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.
This commit is contained in:
simoleo89
2026-05-19 21:48:02 +02:00
parent 05ea0db806
commit 1bad1b20ca
@@ -146,16 +146,17 @@ export class RoomUnitParser implements IMessageParser
user.roomEntryMethod = wrapper.readString(); user.roomEntryMethod = wrapper.readString();
user.roomEntryTeleportId = wrapper.readInt(); user.roomEntryTeleportId = wrapper.readInt();
// NOTE: do NOT read borderId here. `wrapper.bytesAvailable` // Arcturus appends a trailing borderId int per user
// is a boolean meaning "any bytes left in the whole packet?", // (RoomUsersComposer, after the Infostand Borders feature)
// not "any bytes left in THIS user". For users that aren't // for every record — habbo, bot, rentable bot — using 0 as
// the last in the roster, bytesAvailable === true because // the constant for the records that have no border. The
// the NEXT user's bytes follow — reading an int would steal // read MUST be unconditional: a bytesAvailable guard would
// 4 bytes from the next user and cascade-corrupt the entire // be semantically wrong here (the guard answers "any byte
// roster (users not seeing each other on first sight). The // left in the whole packet?" not "any byte left for THIS
// border id for an individual user arrives via // user"), and skipping the read would leave 4 bytes per
// RoomUnitInfoParser (single-user packet), where the // record and cascade-corrupt every subsequent user in the
// bytesAvailable guard is safe because there is no loop. // roster.
user.borderId = wrapper.readInt();
i++; i++;
} }