Arcturus' Habbo.connect() runs when the SSOTicket packet is handled and needs the machineId, which is set by the UniqueID packet. Sending the SSO ticket first made such servers reject the login (WS closed with "Bye") because the fingerprint hadn't arrived yet. Send UniqueID before the SSO ticket so the machineId is available when the server processes the login.
Add FurniEditorImportTextComposer (outgoing 10049) + FurniEditorImportText
ResultEvent/Parser (incoming 10049: found, name, description, classname),
register both in NitroMessages and export from the furnieditor barrels.
Lets the editor pull official names/descriptions from a server-fetched
Habbo furnidata URL.
Append optional sort field + direction to FurniEditorSearchComposer so
the server can order the full result set (not just the visible page).
Defaults id/asc keep existing callers working.
Outgoing 9127-9129: send-hotel-alert (message string), get-dashboard
(no args), list-action-log (limit int).
Incoming 9206 HousekeepingDashboardEvent + 9207 ActionLogEvent with
matching parsers and data classes. Dashboard is a flat one-shot
parse — no count prefix; action log uses the standard "count + N
entries" list pattern.
Closes the HK packet surface — yarn compile:fast clean.
OutgoingHeader 9117-9120: give-credits, give-currency (generic across
duckets/diamonds/seasonal via a currencyType int), grant-item,
set-hc-subscription. All four ride the existing
HousekeepingActionResultEvent — no new parser needed.
`yarn compile:fast` clean.
* Outgoing 9110-9116: find-room-by-id, search-rooms (exact|prefix),
room-state (open|close toggle), mute-room, kick-all-from-room,
transfer-room-ownership, delete-room.
* Incoming 9202 HousekeepingRoomDetailEvent + 9203 RoomListEvent.
* HousekeepingRoomData parser data class with the 11 IHousekeepingRoom
fields. Single-room and list events share the same data class via
composition.
`yarn compile:fast` clean.
Three composers closing out the users-domain HK actions:
* OutgoingHeader 9107 HousekeepingSetUserRankComposer (userId, rankId)
* OutgoingHeader 9108 HousekeepingTradeLockUserComposer (userId, hours, reason)
* OutgoingHeader 9109 HousekeepingResetUserPasswordComposer (userId)
All three ride the existing HousekeepingActionResultEvent for the ack.
OutgoingHeader 9104 HousekeepingMuteUserComposer — (userId, reason,
minutes). 9105 HousekeepingKickUserComposer — (userId, reason). Both
ride the existing HousekeepingActionResultEvent for the ack, so no
new parser is needed.
vitest 138/138, `yarn compile:fast` clean.
HousekeepingUnbanUserComposer (OutgoingHeader 9103) carrying a single
userId int. Response side reuses HousekeepingActionResultEvent — no
new parser needed because the ack shape is action-agnostic.
`yarn compile:fast` clean.
* HousekeepingBanUserComposer (OutgoingHeader 9102): (userId,
reason, hours).
* HousekeepingActionResultEvent + Parser (IncomingHeader 9201):
generic ack carrying (actionKey, ok, actionId, message). Same
parser will back mute / kick / give-credits / room-close / etc.
callers — adding a new HK action only needs a new outgoing
composer plus the right ACTION_KEY constant on the server side.
vitest 138/138, `yarn compile:fast` clean.
OutgoingHeader.HOUSEKEEPING_FIND_USER_BY_ID = 9101 with a one-int
payload. The response side reuses HousekeepingUserDetailEvent (no new
parser) — find-by-id and find-by-name converge on the same shape
because the server has nothing different to say about a user found
via numeric id vs. via username lookup.
vitest 138/138, `yarn compile:fast` clean.
TS counterparts to Arcturus' new HK packet pair. Adds
HousekeepingFindUserByNameComposer (OutgoingHeader 9100) and
HousekeepingUserDetailEvent (IncomingHeader 9200) with a parser that
wraps an optional HousekeepingUserDetailData. The data class follows
the flat optional-trailing-field pattern (isMuted / isTradeLocked
read under bytesAvailable guards) so the renderer stays compatible
with a server that hasn't surfaced those manager APIs offline yet.
The parser exposes `found: boolean` and `user: HousekeepingUserDetailData | null`
so a callsite that gets a "user not found" reply can branch without
having to read into an unpopulated data object — the composer writes
`appendBoolean(false)` and stops, the parser sees the false and leaves
`user` null.
Headers 9100..9199 / 9200..9299 reserved for the rest of the HK
packet surface. Composer + event registered in NitroMessages alongside
the existing YouTube-room overrides. `yarn compile:fast` clean, vitest
138/138 green.
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.
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.