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.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++;
}