Design for sourcing furni display names from furnidata JSON (DB keeps technical data), with a live delta-broadcast pipeline (emulator file-watch -> renderer patch -> client refresh) and a security hardening section. Cross-repo reference copy.
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.