Adds a compact collapsible radio widget (top-left) that plays internet
radio streams with the HTML5 Audio API — no server/renderer changes.
- station list loaded from a JSON5 config file (loadGamedata: JSON + JSON5),
shipped as radio-stations.json5.example so each hotel fills in its own
- shows the selected station + a dropdown (3 visible, scrolls if more) to
switch; volume slider; animated equalizer + LIVE indicator
- first station autostarts quietly (5%) on load, with a resume-on-first-
gesture fallback for browser autoplay policy
When the server (soundboard_sounds table) returns no pads, the client now
loads them from a JSON5 config file (loadGamedata accepts plain JSON and
JSON5). Useful when the DB / CMS isn't set up yet.
File-defined pads play locally for the clicker; DB-backed pads still go
through the server broadcast so everyone in the room hears them. Ships a
radio-style soundboard-sounds.json5.example template.
The editor never requested occupied tiles, so tiles holding furniture
were indistinguishable from empty floor and could be edited/voided.
- request GetOccupiedTilesMessageComposer when the editor opens
- handle RoomOccupiedTilesMessageEvent -> SET_OCCUPIED_TILES
- new Tile.occupied flag (kept separate from `blocked`/void): occupied
tiles render with a distinct marker and are protected from PAINT/
ERASE/ADJUST and brush-to-selection edits
- occupied is purely informational and never changes the saved tilemap
(no accidental voiding of floor under furni)
Tests: reducer cases for SET_OCCUPIED_TILES + edit protection; container
test asserts the occupied event is non-destructive on save; route the
canvas pointer test through elementFromPoint (jsdom has no getScreenCTM).
Client side of the soundboard. Room owners enable it in Room Settings >
Misc (next to the YouTube TV toggle). When enabled, a soundboard icon
appears in the toolbar for everyone in the room; pressing a pad broadcasts
the sound so all occupants hear it. Incoming SoundboardPlay is played via
the HTML5 Audio API.
Also: fix FloorplanCanvasSVG to use ReactElement instead of the removed
global JSX namespace (React 19), and pair the client Dev branch with the
renderer fork that carries the custom features in CI.
How sounds are managed (works with any CMS):
Sounds are rows in the `soundboard_sounds` table:
id, name, url, enabled, sort_order
The emulator loads every row with enabled=1 (ordered by sort_order, id)
and sends the list to clients on room enter; the client plays `url`
directly, so any publicly reachable audio URL works (mp3/ogg/wav).
To add a sound from an admin/housekeeping panel of any CMS:
1. Upload the audio file to wherever the CMS stores public assets
(same approach as custom badge images).
2. INSERT a row into `soundboard_sounds` with the display name and the
public URL of the uploaded file, enabled = 1.
3. Reload the emulator soundboard (or restart) to pick it up.
Relative urls resolve against the `soundboard.url.prefix` config key
(falls back to `asset.url`); absolute urls are used as-is.
NavigatorView reads searchResult/isFetching from useNavigatorSearch
instead of useNavigatorData/useNavigatorUiState. Tab clicks call
setTab(code) on the UI store, which atomically updates the query key
and triggers refetch. The 4 lifecycle useEffect blocks driving the
old imperative flow (needsSearch / reloadCurrentSearch / markReady)
are removed — the query handles all of it now.
NavigatorSearchView has a debounced (300ms) onChange -> setFilter
that drives the same query refetch. Explicit submit (Enter / button)
skips the debounce and calls setFilter immediately.
linkTracker case 'search' now setTab + setFilter + show — no more
pendingSearch ref.
useNavigatorSearch.test.tsx: cast constructors as any to satisfy tsgo
against real renderer types while keeping runtime stubs no-arg-safe.
yarn typecheck / test / lint:hooks all clean (only pre-existing
floorplan environmental failures).
P2 core surgery: search result + NavigatorSearchEvent listener +
sendSearch + reloadCurrentSearch all leave useNavigatorStore. The new
useNavigatorSearch query hook owns the cache. useNavigatorActions is
deleted entirely — the only two actions it exposed are gone, and no
consumer outside Navigator depended on it.
NavigatorMetadataEvent handler now seeds the UI store's currentTabCode
on first arrival, activating the query the moment top-level contexts
land.
useNavigatorData: searchResult removed from closure and return.
useNavigatorUiState: currentTabCode + currentFilter added.
index.ts: useNavigatorActions removed, useNavigatorSearch added.
NavigatorView.tsx is intentionally broken at this commit and gets
fixed in the next.
useNitroQuery keyed on [currentTabCode, currentFilter] from
navigatorUiStore. Fires NavigatorSearchComposer; subscribes to
NavigatorSearchEvent with an accept-filter that rejects results whose
code does not match the current tab. Invalidates on FlatCreatedEvent
and RoomSettingsUpdatedEvent for server-driven refresh.
nitro-renderer.mock.ts: add connection.send stub to GetCommunication
so SendMessageComposer (which calls GetCommunication().connection.send)
does not throw in tests that exercise useNitroQuery.
TDD: 7 cases incl. enabled-gating, accept-filter rejection on
mismatched tab, invalidator round-trip.
setTab(code) atomically updates currentTabCode and resets currentFilter
to '' — switching tabs starts a fresh search context. setFilter(value)
updates only the filter — the user is typing in the same tab.
TDD: 3 new cases (16 total in navigatorUiStore.test).
Each of the 5 Navigator sub-views (RoomCreator, DoorState, RoomInfo,
RoomLink, RoomSettings) is now wrapped in its own WidgetErrorBoundary so
a crash inside one no longer takes down the others. Matches the pattern
already applied to the 13 room widgets + 20 furniture widgets.
Zero behavioural change in the happy path. yarn typecheck +
yarn test --run + yarn lint:hooks all clean (only the 3 pre-existing
floorplan failures remain, unrelated to Navigator).
Commit 8ab0021a introduced an unjustified deviation: it removed the
useNotification() call from inside useNavigatorStore and replaced it
with a module-level _simpleAlert ref + _injectSimpleAlert() exported
function, on the theory that nested useBetween calls corrupt
use-between's state.
That diagnosis is wrong. Production proof:
- useCatalog.ts:56 calls useNotification() inside useCatalogStore
- useWiredToolsStore.ts:131 calls useNotification() inside its store
- The original useNavigator.ts:32 calls useNotification() inside its
state closure
All three have been in production for ages without issue. Nested
useBetween calls work fine.
The smoke-test failure that prompted the workaround was a mock issue,
not a real bug. Reverting to the standard pattern — useNotification()
direct inside the useBetween store closure. Production alerts work
again immediately without requiring an explicit injection call from
consumers.
Mock additions (src/nitro-renderer.mock.ts):
- Added 23 notification MessageEvent subclasses (AchievementNotification-
MessageEvent, ActivityPoint..., BadgeReceived, ClubGiftNotification,
ClubGiftSelected, ConnectionError, HabboBroadcast, HotelClosedAndOpens,
HotelClosesAndWillOpenAt, HotelWillCloseInMinutes, InfoFeedEnable,
MaintenanceStatus, ModeratorCaution, ModeratorMessage, MOTD,
NotificationDialog, PetLevel, PetReceived, RespectReceived, RoomEnter,
SimpleAlert, UserBanned, WiredRewardResult) so useNotificationStore
can register its listeners without throwing.
- Added RoomEnterEffect stub (isRunning: false, totalRunningTime: 0).
- Added WiredRewardResultMessageEvent static constants.
Splits the 492-line useNavigator god-hook into a useBetween-backed
useNavigatorStore closure plus three flat-shape filters
(useNavigatorData, useNavigatorUiState, useNavigatorActions), mirroring
the wired-tools layout. sendSearch + reloadCurrentSearch are extracted
as named actions out of NavigatorView locals.
Door-mode handling is removed from this store and lives in useDoorState
(committed previously) - see GetGuestRoomResultEvent and
GenericErrorEvent dual-subscription with mutually exclusive filters.
The simpleAlert dependency is lifted out of the useBetween scope via a
module-level _simpleAlert ref + _injectSimpleAlert() to avoid nested
useBetween calls that corrupt use-between's module-level dispatcher
state. The ref is null in tests (no events fire during smoke tests) and
is populated in production by the navigator consumer before any alert
is needed.
The barrel index.ts no longer re-exports useNavigator. The 13 consumers
will fail typecheck until the next commit migrates them; the hook files
themselves are clean. Smoke test covers filter shapes.
INTENTIONAL INTERMEDIATE-BROKEN COMMIT: yarn typecheck is RED at this
SHA on the 13 consumer files. The next commit (consumer migration sweep)
brings it back to green.
Code review of Task 2 (commit 07bbc0c7) found two real issues:
1. The GetGuestRoomResultEvent handler did not handle parser.roomEnter,
so after the consumer migration (Tasks 5-8) a successful room entry
would no longer dismiss the door dialog. Fix: reset to INITIAL when
parser.roomEnter is true, before the roomForward branch.
2. The test suite was order-dependent — the useBetween singleton
persisted state across tests, so 'exposes the initial NONE snapshot'
passed only because it ran first. Fix: beforeEach renders the hook
once, calls reset(), then unmounts; afterEach calls cleanup().
Plus one new test case verifying the roomEnter -> reset behavior.
- Add `src/hooks/rooms/widgets/useDoorState.ts`: useBetween-based
singleton wrapping DoorbellMessageEvent / RoomDoorbellAcceptedEvent /
FlatAccessDeniedMessageEvent / GenericErrorEvent /
GetGuestRoomResultEvent; all 5 handlers wrapped in useCallback([])
so their references are stable across useBetween tick() calls and
the effect dep-array never triggers re-registration.
- Add `src/hooks/rooms/widgets/useDoorState.test.tsx`: 11-case Vitest
suite (initial state, 5 event transitions, 2 no-op guards,
GetGuestRoomResultEvent doorbell/password paths, reset()).
- Extend `src/nitro-renderer.mock.ts`: new MessageEvent base class with
callBack/type/getParser; DoorbellMessageEvent / RoomDoorbellAcceptedEvent /
FlatAccessDeniedMessageEvent / GenericErrorEvent / GetGuestRoomResultEvent
concrete stubs; RoomDataParser.DOORBELL_STATE + PASSWORD_STATE; separate
msgListeners map (cleared independently of NitroEvent listeners so
useBetween subscriptions survive between test cases); WeakMap wrapper
for correct removeMessageEvent; GetCommunication routes to msgListeners.
All 11 useDoorState tests pass; full suite 453/456 (3 pre-existing
FloorplanCanvasSVG jsdom/SVG-CTM failures unrelated to this task).
Hoists the 9 useState in NavigatorView (isVisible, isReady, isCreatorOpen,
isRoomInfoOpen, isRoomLinkOpen, isOpenSavesSearches, isLoading, needsInit,
needsSearch) into a createNitroStore-backed Zustand store with named
actions. Future linkTracker / lifecycle wiring will call these actions
instead of mutating local component state.
TDD: 14 cases on each action's transitions + idempotency.
The left-nav container is `max-w-[calc(50vw-242px)]` (reserves the chat
frame width) and uses `overflow-x: clip`. With the full icon set
(habbo, rooms, game, catalog, buildersclub, inventory, ME, wired-tools,
camera, youtube, modtools, furnieditor, housekeeping) the icons exceed
the available 528-608px around the 1540-1700px viewport range, so the
last icons get silently clipped on the right.
Raising the desktop breakpoint from 1540px to 1700px makes the client
fall back to the mobile-scrollable layout (`.tb-bar-scroll`) below
1700px, which scrolls horizontally and doesn't clip.
Above 1700px the desktop fixed-icon layout still applies, now with
enough horizontal room for every icon even with mod+HK enabled.
Touch devices are unaffected (already forced onto the mobile layout
via `pointer: coarse`).
Earlier rev had the hand first, before the label. Feedback: the
label belongs at the very start of the strip; the hand reads
better as the first of the tool buttons it groups with. Same
gesture and exclusive-group behaviour, just visually:
Modalita disegno [hand] [SET][UNSET][UP][DOWN][DOOR] ...
Two related changes from the latest feedback:
1) Hand is now the FIRST button in the toolbar (left of the
'Modalita disegno' label), matching where users typically
look for a pan affordance in painting / mapping editors.
2) The hand and the brush buttons form one exclusive tool
group: picking any brush (SET / UNSET / UP / DOWN / DOOR)
- or select-all / square-select - clears pan mode. No more
'I clicked SET but the canvas keeps panning'. Same goes
the other way: clicking the hand stays sticky, and while
it's active the brush highlights are visually de-selected
even though state.brush.action still holds the last brush
(so the user gets it back the moment they pick a brush
again).
Implementation: replaced the toolbar's onTogglePanMode prop
with an imperative setPanMode(next: boolean) =>. Every other
tool's onClick calls exitPan() first; the hand calls
setPanMode(!panMode) directly. data-active and the border
highlight on the brush + square-select buttons now require
!panMode so the visual state mirrors the gesture state.
No reducer changes - panMode stays a canvas-level UI flag.
Feedback was the amber thumb looked generic / off-the-shelf
and didnt visually tie to the gradient. The thumb now picks
its fill from tileFill of the selected height, so picking 0
shows a blue bead, picking 12 a green one, picking 26 a
purple one, and so on across the full HEIGHT_SCHEME palette.
- Fill: radial gradient on the band colour with a soft white
highlight at top-left and a darker rim at the bottom-right
for a beaded look. The highlight intensity adapts to the
base colour (stronger on dark hues, dimmer on light) so
it never washes out.
- Text contrast: a perceptual-luma heuristic (Rec.601, plain
arithmetic, no colour lib) flips between text-zinc-900 and
text-white at the right threshold so the height number
stays legible on every colour the picker can land on. A
matching textShadow seals the deal on the borderline hues.
- Ring on drag is now zinc-900 + scale-110 (clear gesture
feedback even when the underlying colour is similar to
white).
- Test added: thumb fill at h=0 must differ from h=13, so any
future regression that pins the thumb to a single colour
fails the suite.
Two related polish improvements after the swatch-column → vertical-
slider swap.
Slider
- Wider track (18 px, was 14 px) for a more comfortable click area
with the same on-screen footprint.
- Min / max chips above and below the rail (HEIGHT_BRUSH_MIN /
_MAX) so users know which end is high and which is low without
hovering to discover.
- Thumb now uses a warm amber radial gradient (#fff7c4 → #facc15
→ #ca8a04) on a dark brown border with a soft drop shadow + inset
highlight, instead of the flat yellow disc. Hover adds a white
ring; drag swaps it for a darker ring — clear gesture feedback.
- Track gains a hover/drag glow (inset white seam + amber outline
via boxShadow) so you can tell the slider has focus before you
even click.
Hand tool (canvas pan)
- New FloorplanToolbar button (FaHandPaper, sticky toggle, emerald
fill when active) ties to a new state lifted into
FloorplanEditorView. When the hand is active, plain left-click
+ drag pans the canvas instead of brushing tiles. Cursor flips
to grab / grabbing accordingly.
- FloorplanCanvasSVG's isPanGesture predicate becomes:
middle-mouse OR Shift+left-click OR (panMode && left-click).
Shift / middle still work whether or not the hand is on so power
users keep their muscle memory.
- No change to the reducer (panMode is a canvas-level UI flag, not
a brush action — keeps state/types tight).
Replaces the SVG column of 27 colour swatches with a vertical
slider that fills the same role (pick a brush height 0-26) but
much faster to scrub:
- Track is a discrete-step linear gradient built from the real
tile-fill colours, top = HEIGHT_BRUSH_MAX, bottom =
HEIGHT_BRUSH_MIN. Each height occupies a clear band so the
user still reads colour-to-height at a glance.
- Yellow circular thumb shows the current value as a number,
centred at the picked height's band, with a darker border
while dragging so the drag affordance is obvious.
- Click anywhere on the track to jump; the same gesture starts
a drag (pointermove on window) so users can scrub up/down
without releasing. Pointer-cancel + button-other-than-0 are
handled.
- ARIA: slider role + valuemin / valuemax / valuenow, plus a
touch-none style so mobile scrolling doesn't fight the drag.
Tests rewritten around the new contract (5 cases):
- thumb renders with the current value;
- click at top -> picks 26;
- click at bottom -> picks 0;
- click at middle -> picks 13;
- click at the band that's already selected -> no onSelect
call (idempotent).
Track geometry is stubbed via getBoundingClientRect so the
pointer math is reproducible under jsdom. afterEach(cleanup)
keeps multiple renders from colliding on the data-testid lookup.
Complete modernization of the floor-plan editor. Three layered
changes shipped together since they share state shapes and the
test infrastructure stubs.
1) React rewrite (state + hooks + views + tests)
Drops the FloorplanEditorContext singleton + legacy view
components and replaces them with a pure-React reducer
architecture:
- state/ — typed FloorplanState + FloorplanAction union,
pure reducer covering PAINT_TILE / ERASE_TILE /
ADJUST_HEIGHT / SET_DOOR / SET_DOOR_DIR / SET_THICKNESS /
SET_WALL_HEIGHT / BRUSH_SET / SELECT_RECT / SELECT_ALL /
CLEAR_SELECTION / SQUARE_SELECT_TOGGLE / IMPORT_STRING /
APPLY_REMOTE_DIFF / APPLY_REMOTE_SNAPSHOT. Source-tagged
('local' | 'remote') so the editor can distinguish user
edits from server pushes. Co-located encoding helpers
(parseTilemap / serializeTilemap) and area-counter
selectors.
- hooks/ — useFloorplanReducer (wraps useReducer with a
history stack + loadFromServer + undo/redo), useTool
(pointer events -> dispatch), usePointerToTile (screen
-> tile projection that respects the viewBox origin so
pan/zoom stays accurate).
- views/ — FloorplanCanvasSVG, FloorplanHeightPicker,
FloorplanToolbar, FloorplanOptionsPanel,
FloorplanImportExport, FloorplanTile,
FloorplanPreviewSVG (alternative iso preview kept as a
fallback view, not wired into the main layout).
- Co-located Vitest suites for every module above (encoding,
reducer, selectors, hooks, views, integration). 100+ new
test cases.
2) Live in-room preview (NEW capability)
useFloorplanLiveSync drives client-side preview of the edit
directly into the active room — every tile / door / wall
height / thickness change is applied through
GetRoomMessageHandler().applyFloorModelLocally (new public
method on the renderer, see paired renderer PR) with
zero server traffic during editing. The wire
UpdateFloorPropertiesMessageComposer is only sent when the
user explicitly clicks Save. Thickness slider additionally
calls RoomEngine.updateRoomInstancePlaneThickness for
zero-latency wall/floor-depth feedback while dragging.
Toggle 'Live preview ON / OFF' in the bottom strip (default
ON) lets the user opt out if they want to keep changes
contained to the editor's own preview until Save.
Revert button re-applies the original snapshot locally so
the room snaps back to where it was when the editor opened.
3) UX polish
- Undo / Redo (Ctrl+Z, Ctrl+Shift+Z / Ctrl+Y) backed by a
100-step history stack inside useFloorplanReducer. Local
mutating actions push history; brush/selection UI bumps
and remote dispatches bypass it; loadFromServer wipes the
stack.
- Zoom 40-600 % with Ctrl+wheel, +/- buttons, % label.
Shift+drag or middle-mouse drag pans the canvas.
- Auto-fit on first paint: computes the screen-space
bounding box of the painted (non-blocked) tiles, picks the
zoom that just contains them with a 5 % margin, pans so
the room sits in the viewport centre. Default view is now
'room fills the canvas' instead of 'room is a dot at the
top-centre of a huge empty canvas'. Clicking the % label
re-runs the fit; crosshair button keeps zoom and recentres
the pan only.
- Door direction control: arrows + door icon triplet
(8-way rotate by single click on prev/next, full cycle
forward on the icon itself). Wall and floor thickness
collapse from two 4-button rows into two compact
segmented selectors (active state in emerald). Saves
significant horizontal space.
- Habbo floor pattern tile (~186 B PNG, vendored from
habbofurni.com/images/furni_floor.png) tiled as the
canvas background with image-rendering: pixelated so the
texture stays crisp at every zoom level. Replaces the
solid black background.
Test infrastructure
nitro-renderer.mock grows constructors / proxies / functions
for everything the new floor-editor tests transitively
import (floor composers + events, RoomEngineEvent,
ILinkEventTracker, convertNumbersForSaving /
convertSettingToNumber, GetRoomMessageHandler,
GetTicker, GetRenderer, NitroTicker, RoomPreviewer with a
sufficiently real .updatePreviewModel / dispose surface,
and a TextureUtils.createRenderTexture that returns an
object with a no-op .destroy). test-setup adds a no-op
ResizeObserver polyfill (jsdom doesn't ship one and the
optional FloorplanRoomPreview observes its container) and
a draggable-windows-container portal root for tests that
mount NitroCardView.
Files: 44 changed (mostly new). yarn typecheck 0 errors,
yarn test 341/341 green.
Two things in one commit because they sit on top of each other:
1. **Reset password reveal card.** The emulator's
`HousekeepingResetUserPasswordEvent` already returns the freshly
generated 12-char plaintext in the action-result `message`, but
the client was leaking it through the standard success-banner
pipeline — auto-dismiss in 4s, truncated, no copy button. Operators
were missing it.
- New `<HousekeepingPasswordReveal />` card mounted in the panel
header (between the status banner and tab content). Stays put
until manually dismissed.
- `useHousekeepingStore` gains a dedicated `passwordReveal` slot
(`{ userId, username, password }`) plus `revealPassword()` /
`clearPasswordReveal()` setters. Sensitive data, kept OUT of the
generic banner / toast pipeline.
- `useHousekeepingActions.resetUserPassword` no longer routes
through `wrap()` — it intercepts the result, lifts the
plaintext into the reveal slot, and uses a localizable success
key (`housekeeping.action.reset_password.done`) for the banner so
the password itself never lands there.
- Copy button uses `navigator.clipboard.writeText` in secure
contexts with a `document.execCommand('copy')` fallback for
http:// deployments. Confirmation icon flips to a checkmark for
~1.6s on success. The input is `select-all` + auto-select on
focus so Ctrl+C is also a manual fallback.
- 8 new i18n keys (EN + IT, .example + runtime UITexts.json5 /
UITexts.en.json5).
2. **Catalog admin cleanup ported from the PR branch.** The dev
branch was still carrying the catalog admin code (handlers, hooks,
store slots, i18n keys) even though the local renderer is on the
catalog-stripped `feat/housekeeping-packets` branch — typecheck
was breaking because the catalog composers no longer exist on the
linked renderer. Stripped here to match: 4 catalog actions
removed from `HousekeepingActionType`, `HousekeepingApi.ts`,
`useHousekeepingActions`, `useHousekeepingStore`. The CATALOG tab
id is gone from `HousekeepingTabId`. Catalog interfaces
(`IHousekeepingCatalogPage` / `IHousekeepingCatalogOffer`) are
dropped. 17 catalog i18n keys removed per locale. Two test files
updated to match.
Adds the Housekeeping in-client admin panel — a Modtools-adjacent
surface that runs entirely inside the React client, talking to the
emulator over the existing wire instead of a separate REST/CMS layer.
Surface:
- `src/components/housekeeping/` — panel shell + 5 tabs (Dashboard,
Users, Rooms, Economy, Audit). Each tab drives one domain of the
matching emulator handlers (find/sanction/admin/economy/catalog/
hotel-wide).
- `src/api/housekeeping/` — composer/parser orchestration:
`HousekeepingApi.ts` exposes 30+ typed actions, each one running
through `runHkAction()` which awaits the shared
`HousekeepingActionResultEvent` correlated by action key.
- `src/hooks/housekeeping/` — `useHousekeeping` (the public hook),
`useHousekeepingStore` (useBetween singleton: shared selection +
audit polling + sanction templates), `useHousekeepingActions`,
`useHousekeepingConfirm`.
- `src/api/nitro/awaitMessageEvent.ts` — Promise adapter over
`CommunicationManager.subscribeMessage` with a sync `select`
callback that snapshots the parser INSIDE the subscribe handler
before the renderer recycles the parser instance after the
Promise resolves.
- `public/configuration/housekeeping-texts-{en,it}.example` —
149 EN + 149 IT i18n keys under `housekeeping.*` for every panel
string + every server-side error slug the emulator may emit.
Wiring (additive only):
- `src/components/MainView.tsx` — `<HousekeepingView />` mounted
alongside `<ModToolsView />`.
- `src/api/index.ts`, `src/hooks/index.ts`, `src/api/nitro/index.ts`
— added the `housekeeping` and `awaitMessageEvent` re-exports.
Wire contract: pairs against the Arcturus PR (#120 on
duckietm/Arcturus-Morningstar-Extended) and the renderer PR (#77 on
duckietm/Nitro_Render_V3). Incoming events 9100..9129, outgoing
composers 9200..9207. Permission gate `acc_housekeeping` enforced
server-side; the panel is hidden client-side via
`housekeeping.enabled` in the runtime ui-config.