Commit Graph

232 Commits

Author SHA1 Message Date
medievalshell 9ece87240e feat: branding furni image position + scale (MPU background editor)
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
2026-05-28 15:29:42 +02:00
medievalshell 238592cd72 feat: soundboard packets
Add the soundboard message protocol mirroring the Arcturus side:
- incoming SoundboardSettings (enabled flag + sound list) and
  SoundboardPlay (soundId, url, username) events + parsers
- outgoing SoundboardPlay (soundId) and SoundboardSetEnabled composers
- header ids 9405/9406 (incoming), 9306/9307 (outgoing)
- NitroMessages registration + barrel exports
2026-05-28 09:02:57 +02:00
medievalshell 87eec0563d feat: rare values + fortune wheel protocol + prize editor
Composers/parsers/events for rare values + wheel (open/spin/buy/data/result/
recent-wins) + admin (get/save prizes), headers 9300-9305 / 9400-9404.
fix: figure map uses split-aware loadGamedata (raw fetch broke on tier-manifest
gamedata, silently empty avatars).
2026-05-28 02:39:01 +02:00
duckietm b7688f9d2b 🆕 Added Pickup furni to the floorplan 2026-05-27 09:41:18 +02:00
DuckieTM 72c9564488 Merge pull request #78 from simoleo89/pr/floor-editor-live-preview
feat(room): RoomMessageHandler.applyFloorModelLocally for live floor-plan editor preview
2026-05-26 13:21:56 +02:00
DuckieTM 0a6afd1742 Merge pull request #77 from simoleo89/feat/housekeeping-packets
feat(communication): Housekeeping in-client admin packet surface
2026-05-26 10:50:13 +02:00
DuckieTM 4ddd4bb93d Merge branch 'Dev' into merge-duckie-main-2026-05-06 2026-05-25 18:48:34 +02:00
Lorenzune 22a6d0b3d2 Add total badge count to user profile parser 2026-05-25 10:55:26 +02:00
simoleo89 2504aea85f fix(room): guard RoomPreviewer.updatePreviewModel against null _planeParser
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.
2026-05-24 21:14:53 +02:00
simoleo89 28a41ba543 fix(room): applyFloorModelLocally also rebuilds the furniture stacking map
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.
2026-05-24 21:14:52 +02:00
simoleo89 afd0a4fa16 feat(room): RoomMessageHandler.applyFloorModelLocally for live floor preview
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.
2026-05-24 21:14:52 +02:00
simoleo89 5dd5b26bbe feat(communication): housekeeping hotel alert + dashboard + audit log
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.
2026-05-24 16:28:52 +02:00
simoleo89 386bf79ddc feat(communication): housekeeping economy — 4 composers
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.
2026-05-24 16:26:07 +02:00
simoleo89 597cd2402f feat(communication): housekeeping rooms domain — 7 composers + 2 events
* 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.
2026-05-24 16:26:07 +02:00
simoleo89 c6c6cfe04b feat(communication): housekeeping set-rank + trade-lock + reset-password composers
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.
2026-05-24 16:26:07 +02:00
simoleo89 fbe8a02a72 feat(communication): housekeeping force-disconnect-user composer
OutgoingHeader 9106 HousekeepingForceDisconnectUserComposer carrying
(userId, reason). Reuses HousekeepingActionResultEvent for the ack.
2026-05-24 16:26:06 +02:00
simoleo89 370b1fc100 feat(communication): housekeeping mute-user + kick-user composers
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.
2026-05-24 16:26:06 +02:00
simoleo89 c9d8f32e62 feat(communication): housekeeping unban-user composer
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.
2026-05-24 16:26:06 +02:00
simoleo89 31598b8883 feat(communication): housekeeping ban-user + generic action-result
* 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.
2026-05-24 16:26:06 +02:00
simoleo89 3113baf559 feat(communication): housekeeping find-user-by-id composer
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.
2026-05-24 16:26:06 +02:00
simoleo89 ec7e122e74 feat(communication): add housekeeping find-user-by-name packet pair
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.
2026-05-24 16:26:06 +02:00
duckietm 081e06b208 🆙 Catalog Editor, now you can also edit the text1 2026-05-22 11:05:45 +02:00
duckietm bdbb8e7161 🆙 Fix Catalog Edit 2026-05-21 16:59:36 +02:00
duckietm c15b606a4d 🌲 Bump to Version 3.5.0 2026-05-20 11:43:54 +02:00
simoleo89 1bad1b20ca 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.
2026-05-19 21:48:02 +02:00
simoleo89 05ea0db806 fix(parser): drop unsafe borderId read inside the RoomUnitParser per-user loop
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.
2026-05-19 21:05:36 +02:00
simoleo89 cc1e8fe9c7 fix(api): align IRoomSession.sendBackgroundMessage signature with the impl
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.
2026-05-19 20:38:47 +02:00
simoleo89 4be09c6c62 Merge remote-tracking branch 'origin/Dev' into feat/react19-event-bus
# Conflicts:
#	packages/session/src/UserDataManager.ts
2026-05-19 20:33:54 +02:00
simoleo89 221f186d61 refactor(session): fold permission map into UserPermissionsEvent
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.
2026-05-19 19:39:49 +02:00
simoleo89 159c5eb6e8 feat(session): resolved permission map snapshot (USER_PERMISSIONS_UPDATED)
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.
2026-05-19 18:59:35 +02:00
simoleo89 87e67d58df feat(session): rank metadata in UserPermissions snapshot
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.
2026-05-19 18:37:57 +02:00
simoleo89 ce561bd5b3 feat(utils): parallelize gamedata loader + structured fetch errors
Three improvements on top of duckietm/Dev's new JSON5 + split-aware
gamedata loader:

1. Parallel fetches inside loadGamedata: every file declared in a
   tier's manifest is now fetched with Promise.all. The merge step
   still walks the parts in declared order so override semantics
   (core -> custom -> seasonal, and within-tier declaration order)
   are preserved. Root-manifest files and per-tier manifest discovery
   also run concurrently.

2. tryFetchManifest distinguishes 404 from other failures. The
   previous tryFetchOrNull silently treated parse errors and 5xx as
   "manifest missing", so a malformed manifest.json5 made an entire
   tier vanish from the boot. Now only HTTP 404 returns null; every
   other failure propagates.

3. New ConfigJsonError class with phase ('fetch' | 'parse'),
   sourceUrl, and optional httpStatus. Exported isMissingResource()
   helper lets callers check for 404 without string-matching.

Also:
- mergeGamedata warns via NitroLogger when an array looks keyed by
  id/classname/name on >=80% of items but a few are missing the
  key (the previous behavior fell back to concat() and produced
  silent duplicates).
- Removed the dead text === null/undefined branch in parseConfigJson
  (Response.text() never returns null).

Verified: tsgo clean, 138/138 tests pass on the renderer, 207/207
tests pass on the client (no behavioral change to existing callers).
2026-05-19 17:14:13 +02:00
simoleo89 807efcff8f merge: integrate duckietm/Dev (JSON5 + split-aware gamedata loader)
# Conflicts:
#	packages/session/src/SessionDataManager.ts
#	yarn.lock
2026-05-19 17:01:58 +02:00
duckietm 4b7d04d0b8 🆕 Infostand Borders 2026-05-19 16:56:25 +02:00
medievalshell ae9bc8bfce feat(utils): split-aware gamedata loader with tiered merge
Introduces loadGamedata(url, options?) and mergeGamedata(a, b) in
@nitrots/utils. The loader transparently accepts:

- a single-file URL (legacy) -> parsed as before
- a directory URL ending with '/' -> tier-merged from core/custom/seasonal,
  each tier driven by its own manifest.json5

Merge rules:

- arrays of objects sharing an id key (id, classname, name): merged by id,
  later layers overriding earlier ones
- arrays without an id key: concatenated
- plain objects: recursive merge per key
- anything else: later value wins

All gamedata consumers (FurnitureDataLoader, ProductDataLoader,
EffectAssetDownloadManager, AvatarRenderManager actions+figuredata,
LocalizationManager) are migrated to loadGamedata. Behaviour is unchanged
for single-file URLs, so existing deployments need no config changes;
opt-in to split mode by appending '/' to the URL once the layout is in
place.

README updated with the directory layout, merge table and programmatic
usage example. The companion CLI splitter that produces the core/ tier
from legacy files lives in the Nitro V3 client repo.
2026-05-18 21:19:54 +02:00
simoleo89 d740f833eb refactor(parsers): flatten nested bytesAvailable guards on UserProfile + GetGuestRoomResult
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.
2026-05-18 20:57:28 +02:00
simoleo89 892d16b393 feat(sound): snapshot getter + volume-update event on SoundManager
Extends the snapshot pattern to the three audio volume levels (system /
furni / trax) so volume-slider widgets on the React client can subscribe
to a single source of truth via useSyncExternalStore.

API additions on ISoundManager:
- systemVolume / furniVolume getters (parity with the existing
  traxVolume getter)
- getVolumesSnapshot(): Readonly<ISoundVolumesSnapshot> with the same
  lazy-frozen + invalidation-on-change semantics as the user/session
  snapshots
- new ISoundVolumesSnapshot { system, furni, trax } interface

New event: NitroEventType.SOUND_VOLUMES_UPDATED. Dispatched only when
the incoming NitroSettingsEvent.SETTINGS_UPDATED actually changes one
of the three volumes (a no-op refresh stays quiet).

While in there, fixed a real bug: the previous implementation cached
`volumeFurniUpdated` / `volumeTraxUpdated` BEFORE writing the new
values, but read `castedEvent.volumeFurni` / `castedEvent.volumeTrax`
in their pre-division form — comparing percent (e.g. 75) against the
already-divided stored value (e.g. 0.75) — so the change check almost
always reported "updated" for a real settings push and never reported
"updated" if the percent matched the stored fraction by coincidence
(only 0/100 are stable). Updated check is now consistent (compare
fraction vs fraction) and also tracks systemVolume changes for the
new snapshot invalidation.
2026-05-18 20:55:12 +02:00
simoleo89 761d8ffe19 feat(session): snapshot getter for UserDataManager room user list
Extends the snapshot pattern to the room's user list. The React client
currently has many widgets each calling `getUserDataByIndex(idx)` in a
loop (chat, doorbell, room player list, infostand …) — every render
walks the underlying Map and rebuilds an array. With
`getRoomUserListSnapshot(): ReadonlyArray<IRoomUserData>` consumers can
memoize on the array reference and only rebuild when something actually
changed.

Invalidation fires on every state-changing path:
- updateUserData (add/replace)
- removeUserData (leave)
- updateFigure / updateName / updateMotto / updateNickIcon /
  updateCustomization / updateBackground / updateAchievementScore /
  updatePetLevel / updatePetBreedingStatus

The inner IRoomUserData objects keep their existing in-place mutation
semantics (deep-clone would be too expensive for 30+ avatars on every
single status push). Consumers should treat each entry as a
snapshot-at-time-of-read and not retain references across an
invalidation.

New event: NitroEventType.ROOM_USER_LIST_UPDATED. Interface and event
additions are backwards-compatible; no existing accessors changed.

Also tidied: `updatePetLevel` now uses the explicit `if(!userData)
return;` guard pattern matching the rest of the methods (was a one-line
inline conditional).
2026-05-18 20:52:33 +02:00
simoleo89 a599e0cf89 feat(session): snapshot getters for IgnoredUsersManager + GroupInformationManager
Extends the v2.1.0 React-friendly snapshot pattern (originally on
SessionDataManager / RoomSessionManager) to two more session-state
holders the React client reads frequently:

- IgnoredUsersManager.getIgnoredUsersSnapshot(): ReadonlyArray<string>
- GroupInformationManager.getGroupBadgesSnapshot(): ReadonlyMap<number, string>

Both follow the same shape: lazy-frozen snapshot, cached until the
underlying state mutates, then invalidated and a dispatched event lets
the React client rebuild via useSyncExternalStore.

Two new NitroEventType members carry the invalidation signal:
- IGNORED_USERS_UPDATED — dispatched by IgnoredUsersManager whenever
  the list changes (initial load, add, remove, queue-truncate case 2).
- GROUP_BADGES_UPDATED — dispatched by GroupInformationManager only
  when the incoming HabboGroupBadges payload contains at least one
  new or changed mapping (no-op refresh stays quiet).

This lets the user-info popup, profile page, friend/guild filtering,
and any other consumer share a single read through useSyncExternalStore
instead of each subscribing to the underlying message events
independently.

API additions are interface-respecting and backwards-compatible — the
existing `isIgnored(name)` / `getGroupBadge(groupId)` accessors stay
untouched.
2026-05-18 20:50:24 +02:00
simoleo89 98662e7399 test(utils): add BinaryReader / BinaryWriter round-trip coverage (23 cases)
Cover every public method on the binary pair, plus the typical packet
shape (header + mixed payload) the composer/parser pipeline emits:

- byte / short / int round-trips, including signed-edge values
  (int8 -1 from 0xFF, int16 / int32 boundaries)
- big-endian wire-order assertions on writeShort / writeInt (matches
  Arcturus's DataInputStream)
- string round-trip with length prefix + bare (includeLength=false)
  + UTF-8 multibyte byte count + empty-string edge
- writeBytes for both number[] and ArrayBuffer payloads
- readBytes slice returns an independent reader whose position is
  decoupled from the outer reader
- remaining() decrements correctly across mixed-size reads
- readFloat / readDouble decode IEEE-754 big-endian values
  (the writer has no float/double counterparts — buffer is built via
  DataView for these cases)
- writer position getter + explicit setter (caller-managed reposition)
- two independent writers concatenate cleanly into a single reader

Suite: 127/127 (was 104/104). typecheck clean.
2026-05-18 20:42:17 +02:00
medievalshell 2a00365862 feat(utils): honour __NITRO_JSON_MODE__ flag in JsonParser
Adds three explicit parsing strategies selectable at host build time via
the compile-time constant __NITRO_JSON_MODE__:

- legacy: strict JSON.parse only; clear error suggesting JSON5 mode
- json5 : JSON5.parse only
- auto  : try JSON, fall back to JSON5 (existing behaviour and default
          when the flag is undefined, so older hosts keep working)

URL/MIME hints for .json5 sources are still respected. README updated
with the modes table and a Vite wiring example.
2026-05-18 20:37:46 +02:00
simoleo89 820f791e66 Merge remote-tracking branch 'origin/main' into feat/react19-event-bus 2026-05-18 20:26:17 +02:00
duckietm 31df26bd1b 🆕 Added support for JSON5 2026-05-18 16:14:38 +02:00
duckietm b6a26fbd84 🆙 Small fix landscape's where a bit offset 2026-05-12 11:48:06 +02:00
simoleo89 ef6c661058 Renderer: surface allowUnderpass on RoomSettingsData + composer
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.
2026-05-11 21:46:36 +02:00
simoleo89 f7a5897232 Renderer: align NitroConfig Window decl with client + fix glob .default access
Two tsgo nits that propagate to the client when the renderer is linked
in:

- packages/utils/src/NitroConfig.ts declared
  'NitroConfig?: { [index: string]: any }' on Window, but Nitro-V3
  declares 'NitroConfig?: Record<string, unknown>' in its
  react-app-env.d.ts. The two declaration-merging fragments must
  match — switching the renderer side to Record<string, unknown>
  unifies them.
- packages/assets/src/AssetManager.ts: 'import.meta.glob(...)' is
  augmented as Record<string, string> in the client's typedef, so
  'mod.default ?? mod' (defensive handling of either string or
  { default: string }) failed because mod is typed string. Cast
  inline: '((mod as { default?: string }).default ?? mod)'.

Renderer tsgo error count: 3 -> 0.
2026-05-11 21:34:47 +02:00
simoleo89 22d4e5bfb0 SocketConnection parser cast + RoomChatHandler arg-order fix
- 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.
2026-05-11 21:10:04 +02:00
simoleo89 5ea3201e31 Align with Pixi v8: Filter[] union, WebGLRenderer narrow, ImageLike
Four sites where Pixi v8's stricter typing tripped tsgo:

- AvatarImage: container.filters is typed as 'readonly Filter[] | null'
  in v8 (no longer a single-Filter union). The old fallback branch
  'else container.filters = [container.filters, …]' tried to treat a
  readonly array as a single Filter; collapsed to the array-spread
  path which now covers both undefined and non-empty cases. Added
  Filter to the pixi.js import.
- FurnitureBadgeDisplayVisualization.updateSprite() had a 4-arg
  override (sprite, asset, scale, layerId) of the parent's 2-arg
  signature (scale, layerId). The sprite/asset were never used from
  the parameters — the body only mutated 'sprite'. Refactored to
  fetch the sprite via this.getSprite(layerId) inside the override
  body so the signature matches the base.
- ExtendedSprite: 'renderer.gl' / 'glRenderTarget.resolveTargetFramebuffer'
  exist only on WebGLRenderer / GlRenderTarget (not the WebGPU
  variants). The runtime check 'renderer.type === RendererType.WEBGL'
  guarantees this; cast at the boundary to satisfy the typechecker.
- TextureUtils.generateImage: Pixi v8's Extractor.image() returns the
  union ImageLike (HTMLCanvasElement | HTMLImageElement); the public
  signature promises HTMLImageElement. Cast at return — the Pixi
  default backend returns HTMLImageElement here.
2026-05-11 21:09:59 +02:00
simoleo89 b42f989e28 RoomEnterComposer: optional spawnX/spawnY for reconnect
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
2026-05-11 21:09:51 +02:00
simoleo89 999b8187d6 Fix PetBreedingMessageParser bytesAvailable check
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.
2026-05-11 21:09:41 +02:00