Add SessionDataManager.mergeFurnitureDataFromUrl() + FurnitureDataLoader.mergeFromUrl()
to merge a single furnidata chunk (e.g. a custom tier) into the live floor/wall maps at
runtime, returning the added entries so callers can also refresh the RoomContentLoader.
Lets newly added furniture appear without a full client reload.
tryFetchManifestPair sceglie l'estensione in base a resolveJsonMode():
json5 -> .json5, legacy -> .json, auto -> entrambi. Evita le richieste
manifest.json fallite a ogni avvio in modalita json5.
FurnitureBrandedImageVisualization now adds offsetZ to the branded layer (z-index for the MPU/billboard editor). The room background uses offsetZ as an inverse depth push (the 'play with Z to hide floor/walls' trick); its getLayerZOffset subtracted offsetZ assuming the parent did not add it, so the two cancelled out and the effect was lost. Cancel the parent's +offsetZ for the branded layer to restore the original net (base - offsetZ).
Renderer support for the in-client image position editor:
- FurnitureBrandedImageVisualization applies offsetX/Y to the branded image
layer only (offsetZ stays as z-index/depth), so the image can be moved
without shifting the furni frame
- new `scale` branding key + FURNITURE_BRANDING_SCALE: zooms the image via a
real per-sprite scale (RoomObjectSprite.scale, default 1, applied in
RoomSpriteCanvas) — NOT by writing the read-only width/height
- AssetManager loads external raster images (png/jpg/…) via a CORS <img> +
Texture.from instead of Assets.load (which didn't load cross-origin images);
branding image download failures are now surfaced instead of swallowed
After dispose() nulls out the internal _planeParser /
_backgroundSprite refs, any further updatePreviewModel call
crashed with 'this._planeParser is null'. React 19 StrictMode
in dev double-mounts effects (setup, cleanup, setup again),
which can briefly leave a consumer holding a stale reference
to a disposed previewer between the two setup runs. Bail
silently in that window instead of crashing the editor.
The first cut updated wallGeometry + RoomMapData (so the
visualization rebuilt) but NOT the FurnitureStackingHeightMap.
The stacking map is what governs whether the room treats a
tile as 'a room tile you can stack furni on' vs. 'blocked'.
Without rebuilding it, every newly-painted tile in the live
preview looks walkable but rejects furniture placement -
user reported exactly that.
Mirror the structure of onRoomHeightMapEvent: build a fresh
FurnitureStackingHeightMap from the parsed floor (height +
isRoomTile from FloorHeightMapMessageParser.TILE_BLOCKED),
default stackingBlocked=false, then setFurnitureStackingHeightMap
+ refreshTileObjectMap so the room object map picks up the
new tile set.
Adds a public method that rebuilds the active room's floor
geometry from an in-memory model string + wallHeight without
touching the server. Same pipeline as the wire-driven
onRoomModelEvent (FloorHeightMapMessageParser ->
_planeParser -> wallGeometry), but instead of going through
RoomEngine.createRoomInstance (which is a no-op on a room
that already exists) it routes the resulting RoomMapData
through the room object's ObjectRoomMapUpdateMessage channel
- the same mechanism RoomPreviewer.updateRoomPlanes uses for
its iso preview. Result: the visualization rebuilds in place
and existing furniture/avatars are preserved.
Refactor: extracted the parser->planeParser->wallGeometry-
>RoomMapData work from onRoomModelEvent into a private
_rebuildFloorGeometry(parser) helper so the wire handler and
the new public method share an implementation.
Intended use: tools that need a live in-room preview of a
floor edit before committing it server-side (e.g. the React
floor-plan editor's live-preview mode). The wire
UpdateFloorPropertiesMessageComposer remains the source of
truth - call applyFloorModelLocally only for transient
client-side preview.
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.
Document that the per-user borderId int in RoomUsersComposer (Arcturus
Infostand Borders) must be read unconditionally inside the per-user
loop in RoomUnitParser, never wrapped in a bytesAvailable guard. Calls
out the failure modes on both server shapes (emits / doesn't emit)
explicitly so the next contributor doesn't re-introduce the guard
"for safety" the way it was earlier today. Points future
trailing-int additions at the same parser block for documentation.
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.
The RoomSession.sendBackgroundMessage impl takes 5 args (background,
stand, overlay, card, border) but the interface only declared 4 —
TypeScript consumers calling roomSession.sendBackgroundMessage(...) with
the border arg failed to typecheck even though the runtime call worked.
Add the optional backgroundBorder?: number trailing parameter to the
interface so the contract matches what RoomSession.ts ships.
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.