The Infostand Borders merge (origin/Dev 4b7d04d, upstream commit) added
user.borderId = (wrapper.bytesAvailable ? wrapper.readInt() : 0);
inside the per-user loop in RoomUnitParser (the parser for the
RoomUsersComposer packet — header 3920 — which ships the full roster
on room enter). The guard is unsafe inside a loop: `bytesAvailable`
is a boolean meaning "any bytes left in the WHOLE packet?", not
"any bytes left in THIS user record". For every user except the
last one, `bytesAvailable === true` because the NEXT user's bytes
still follow, so the parser reads an int and steals 4 bytes from
the next user — cascade corruption of the entire roster.
Symptom in production: users don't see each other on first room
sight. The roster arrives, the parser sfasa, RoomEngine drops the
malformed records.
Fix: stop reading borderId inside the loop. The per-user border id
is shipped separately via RoomUnitInfoParser (single-user packet,
no loop), where the bytesAvailable guard is safe. The roster
packet's last-tail extension story stays clean for any future
trailing block the same way other parsers do — but only when the
guard is the LAST read in the packet, not a per-record one.
This also makes the renderer wire-compatible with both old
emulators (no borderId at all) and the new Arcturus version that
ships borderId in RoomUsersComposer — the latter just has 4 extra
trailing bytes per user that the parser ignores. A follow-up change
on Arcturus' RoomUsersComposer can drop the borderId append, or
keep it and the client simply doesn't read it from the roster
(which is fine — the infostand re-fetch via RoomUnitInfoParser
gives the authoritative border).
mvn-equivalent: yarn compile:fast clean, vitest 138/138.
Drop the separate UserPermissionsMapEvent / UserPermissionsMapParser
and the IncomingHeader.USER_PERMISSIONS_MAP = 10070 registration —
the resolved permission map now rides on the existing
UserPermissionsEvent as a third optional trailing block, after the
rank metadata one. Same wire data, one fewer packet, one fewer
event registration, one fewer handler.
Wire layout (UserPermissionsEvent / header 411):
int clubLevel
int securityLevel
bool isAmbassador
--- rank metadata (Arcturus ≥ 4.2.10) ---
int rankId
string rankName
string rankBadge
string rankPrefix
string rankPrefixColor
--- resolved permission map (Arcturus ≥ 4.2.10) ---
int count
loop: string permission_key + int value (1=ALLOWED, 2=ROOM_OWNER)
Both trailing blocks are guarded by `bytesAvailable` in
UserPermissionsParser so older emulators that don't append them
still parse cleanly.
SessionDataManager.onUserPermissionsEvent is now the single handler:
- updates clubLevel/securityLevel/isAmbassador/rank* AND _permissions;
- invalidates BOTH the user-data snapshot and the permissions
snapshot (dispatching the two distinct
NitroEventType.SESSION_DATA_UPDATED / USER_PERMISSIONS_UPDATED
events).
The two distinct invalidation events stay so React consumers can
subscribe granularly — useHasPermission(key) only triggers on a real
permission map flip, not on every session-data bump.
Companion Arcturus change (feat/react19-emu-update) folds
UserPermissionsMapComposer into UserPermissionsComposer and removes
the second sendResponse in HabboManager.setRank +
SecureLoginEvent.
Verification: yarn compile:fast clean, vitest 138/138.
Adds the wire pipeline for `Outgoing.UserPermissionsMapComposer = 10070`
shipped by Arcturus-Morningstar-Extended ≥ 4.2.10. The composer sends
the resolved `permission_definitions` map for the current user
(filtered to ALLOWED / ROOM_OWNER entries) at login and after every
`HabboManager.setRank` — so a runtime promote/demote re-derives every
React-side permission gate.
- NitroEventType.USER_PERMISSIONS_UPDATED — new invalidation event.
- IncomingHeader.USER_PERMISSIONS_MAP = 10070.
- UserPermissionsMapParser reads `int count + (string key, int value)*`.
- UserPermissionsMapEvent + NitroMessages registration.
- SessionDataManager._permissions Map + getPermissionsSnapshot()
referentially-stable per the snapshot convention. New handler
onUserPermissionsMapEvent copies the parser map into the manager
(so the parser's mutable reference doesn't leak) and invalidates.
- ISessionDataManager.getPermissionsSnapshot() — public contract.
React-side consumers ship in the companion commit on
feat/react19-modernization. The wire is backward-compatible: older
emulators never send the packet, the snapshot stays empty Map, and
all useHasPermission(key) gates return false (mod-only UI hidden by
default = safe).
Verification: tsgo clean, vitest 138/138.
Extend the `UserPermissionsEvent` parser and `IUserDataSnapshot` with
rank metadata mirrored from the Arcturus `permission_ranks` table:
rankId, rankName, rankBadge, rankPrefix, rankPrefixColor.
The new fields are appended to the wire payload AFTER the existing
[clubLevel, securityLevel, isAmbassador] triple. The parser guards
the trailing block with `if(!wrapper.bytesAvailable) return true;`
so older emulators (that don't write the extension) keep working —
the snapshot just exposes the defaults (rankId=0, empty strings) in
that case.
`SessionDataManager.onUserPermissionsEvent` stores the values; the
snapshot builder includes them; existing
`invalidateUserDataSnapshot()` semantics flow through unchanged, so
a runtime promote/demote (via `HabboManager.setRank` →
`UserPermissionsComposer`) auto-flips the React-side
`useUserRank()` / `useHasRankLevel()` / `useIsRank()` consumers in
the Nitro-V3 client.
Companion changes:
- Arcturus-Morningstar-Extended:
`UserPermissionsComposer.composeInternal()` now appends the 5
extra fields (pending operator commit; see
../Arcturus-Morningstar-Extended/Emulator/src/main/java/
com/eu/habbo/messages/outgoing/users/UserPermissionsComposer.java).
- Nitro-V3:
`useSessionSnapshots.ts` exposes the new family
(useUserRank / useHasRankLevel / useIsRank), replacing the
SecurityLevel-based wrappers (useIsModerator etc.) that hardcoded
the renderer enum names — those don't match the operator's
`permission_ranks.rank_name` column.
Verification: tsgo clean, vitest 138/138.
Two parsers handle "one tier of optional trailing fields per emulator
release" via nested if(wrapper.bytesAvailable) chains. The semantics
were correct but each new block sat one extra indent deeper than the
previous one, and adding tier N+1 quietly meant re-indenting everything
above it. Replaced with a flat early-return chain that's diff-friendly
when the next emulator version ships a new trailing block:
if(!wrapper.bytesAvailable) return true;
// block N reads
if(!wrapper.bytesAvailable) return true;
// block N+1 reads
…
Functionally equivalent — defaults still come from flush(), older
servers still bail at whichever tier they don't ship. Each block is
now also documented inline so the order/contract is obvious without
cross-referencing Arcturus.
In UserProfileParser, also straightened the cardBackgroundId tier:
was an inline `(wrapper.bytesAvailable ? readInt() : 0)` mid-block;
now a proper `if(!bytesAvailable) return true;` guard between blocks,
matching the rest of the chain.
Arcturus' RoomSettingsComposer appends an extra int at the end of the
payload — room.isAllowUnderpass() ? 1 : 0 — and RoomSettingsSaveEvent
optionally reads back a boolean at the end (if bytesAvailable > 0).
The renderer side never modeled this trailing field, so the client
couldn't surface or persist it.
- RoomSettingsData: add _allowUnderpass field + getter/setter +
propagation through the .from() copy.
- RoomSettingsDataParser: read one trailing int after the moderation
settings, guarded by 'if(wrapper.bytesAvailable)' so older servers
that don't emit it keep parsing cleanly.
- SaveRoomSettingsComposer: optional trailing allowUnderpass arg. The
server's optional-read guard tolerates 24-arg or 25-arg payloads, so
callers that don't care about the field still send the legacy shape.
Cross-repo reference points:
- Arcturus emit side: Emulator/src/main/java/com/eu/habbo/messages/
outgoing/rooms/RoomSettingsComposer.java line 55.
- Arcturus read side: Emulator/src/main/java/com/eu/habbo/messages/
incoming/rooms/RoomSettingsSaveEvent.java lines 133-135.
Net client tsgo error count: 3 -> 0 on the NavigatorRoomSettings cluster.
- SocketConnection.processMessage() did 'new events[0].parserClass()'
where parserClass is typed as 'Function' on IMessageEvent (no
construct signature). Cast to 'new () => IMessageParser' at the
call site so the spawned instance is type-correct downstream.
- RoomChatHandler dispatched RoomSessionChatEvent with the args in
the wrong order: '[]' (intended as the 'links' array) was landing
in the 'chatColours' string slot. Swap to '"", []' so links go
to position 8 and chatColours stays a string.
Arcturus' RequestRoomLoadEvent reads the two extra ints only when
the inbound packet has 8+ bytes remaining after roomId+password, so
the renderer can send 2-arg or 4-arg payloads against the same
header. The client already calls 'new RoomEnterComposer(roomId,
password, spawnX, spawnY)' in two places inside RoomSession /
RoomSessionManager (the reconnect/respawn flow) — the composer
signature is what was lagging behind.
Server-side reference:
Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/
messages/incoming/rooms/RequestRoomLoadEvent.java
bytesAvailable is a boolean (IMessageDataWrapper.bytesAvailable: boolean,
returns 'there is at least one byte left'); the parser was doing
'wrapper.bytesAvailable < 12' as if it were a count, which both
mis-compares boolean to number and short-circuits incorrectly when
exactly 11 bytes remain.
Align with every other parser in the codebase: 'if(!wrapper ||
!wrapper.bytesAvailable) return false;'. The downstream readInt
calls already throw on truncated packets so the explicit length
check was load-bearing only against malformed inputs that wouldn't
parse anyway.
WiredRoomSettingsRequestComposer and WiredUserVariablesRequestComposer
declared 'implements IMessageComposer<ConstructorParameters<typeof Self>>'
but neither defines a constructor, so ConstructorParameters resolved
to 'any[]' and getMessageArray() returning [] (any[]) failed the
narrower base-type signature () => [].
Both composers send zero payload; type as IMessageComposer<[]>
directly + annotate the return type.
TypeScript 5.7 split ArrayBuffer / SharedArrayBuffer at the type level
(ArrayBuffer now exposes resizable/transfer/detached etc; SharedArrayBuffer
doesn't), and parametrized the typed-array constructors so plain
Uint8Array became Uint8Array<ArrayBufferLike>.
The renderer never uses SharedArrayBuffer, so this is type-level only —
narrowing back to ArrayBuffer at the boundaries:
- BinaryReader.readBytes() / .toArrayBuffer() return the underlying
DataView buffer; cast to ArrayBuffer.
- BinaryWriter.getBuffer() same shape.
- WsSessionCrypto.randomNonce() now returns Uint8Array<ArrayBuffer>
(it's always backed by a plain ArrayBuffer); aesGcmEncrypt/Decrypt
nonce parameter retyped accordingly so SubtleCrypto.encrypt accepts
it as BufferSource.
- ArrayBufferToBase64 now accepts Uint8Array | ArrayBufferLike directly
(pako/inflate hands back Uint8Array<ArrayBuffer> which the old
ArrayBuffer-only signature rejected).
Each workspace package was still pinning `typescript: ~5.5.x` or
`~5.8.2` in its own devDependencies even though the root bumped to 6.0.3
in 60b1143. The pins were dead (yarn 1 hoists from root) but they're
misleading when reading a single package.json. Bring them all to
`^6.0.3` to match the root.
Other:
- @thumbmarkjs/thumbmarkjs 1.8.1 → 1.9.0 (root + communication package)
- yarn.lock regenerated from a clean install (vitest 4 hoisting was
flaking on the patch vite bump; reverted vite to ^8.0.10)
Adds CLAUDE.md at the repo root: short project context for future
sessions — stack, the 12-workspace layout, the React-friendly v2.1.0
additions (`subscribe()`, `subscribeMessage()`, snapshot getters), build
scripts, and known gotchas (`SessionDataManager.getUserData` does NOT
exist; sendChat* expects 3 args; dispatchEvent is sync).
- CatalogAdminSavePageImagesComposer (header 10060): pageId, headerImage, teaserImage
- CatalogAdminSavePageIconComposer (header 10061): pageId, iconId
- Registered in OutgoingHeader, catalog index, and NitroMessages
- Server handler required in Arcturus to process these packets