Adds a 'Recent renderer changes' section to CLAUDE.md covering all the non-v2.1.0 work that landed during the React 19 modernization round: - RoomEnterComposer optional spawnX/spawnY (matches Arcturus' RequestRoomLoadEvent optional tail). - RoomSettingsData.allowUnderpass field + parser tail-int + composer optional arg (Arcturus already emits the int). - Dropped dead sendWhisperGroupMessage / ChatWhisperGroupComposer. - TS 5.7+ ArrayBuffer drift handling and Pixi v8 narrows (FurnitureBadgeDisplayVisualization signature realignment, WebGLRenderer cast in ExtendedSprite, Filter[] union in AvatarImage, ImageLike cast in TextureUtils, NitroConfig Window-decl unification, empty-tuple composers). - PetBreedingMessageParser bytesAvailable bool-vs-number bug fix. Also adds two gotchas: 'bytesAvailable is a boolean' (was hit by PetBreeding) and 'composer getMessageArray return type must match the type argument' (was hit by both Wired*RequestComposer).
9.0 KiB
Nitro_Render_V3 — Claude project context
Pure-TypeScript renderer library for the Nitro retro Habbo client. Wraps PixiJS v8 for room/avatar rendering and provides the WebSocket
- event-bus infrastructure that the React client (
../Nitro-V3) sits on top of.
Stack
- TypeScript 6.0 (root) + tsgo (
@typescript/native-preview, TS 7 preview compiler — used byyarn compile:fast, ~7× faster on this codebase) - PixiJS v8 (
pixi.js@8.18) - Vite 8 for build + bundling
- Vitest 4 for unit tests
- Yarn 1.22 workspaces (
packages/*) — note: yarn 1, NOT yarn 4 like the client. The two repos use different package managers on purpose. - No React — this is a pure TS library; React lives in
../Nitro-V3.
Workspace layout
Twelve internal packages under packages/*/src/, each pinning
typescript: ^6.0.3 in its own devDependencies:
packages/
api public interfaces (IEventDispatcher, ISessionDataManager, ...)
assets asset loading + caching
avatar avatar rendering / figure resolution
camera in-room camera widget
communication WebSocket + composer/parser pipeline
configuration runtime config loader
events EventDispatcher + NitroEventType + per-domain events
localization LocalizationManager
room RoomEngine + RoomVisualization
session SessionDataManager + RoomSessionManager + handlers
sound SoundManager (howler-based)
utils shared utilities (BinaryReader, Logger, …)
Root index.ts re-exports everything from @nitrots/* so the React
client gets a flat import { … } from '@nitrots/nitro-renderer'.
React-friendly API additions (v2.1.0)
Three additions matter for the React client integration. Keep these backwards-compatible:
EventDispatcher.subscribe(type, callback): () => void
Signature matches what useSyncExternalStore expects — returns an
unsubscriber, no need to juggle callback identity. Implemented in
packages/events/src/EventDispatcher.ts. The legacy
addEventListener / removeEventListener still work.
CommunicationManager.subscribeMessage(eventCtor, handler): () => void
Equivalent for packet streams. Implemented in
packages/communication/src/CommunicationManager.ts.
Snapshot getters on SessionDataManager + RoomSessionManager
getUserDataSnapshot(): Readonly<IUserDataSnapshot>
getActiveRoomSessionSnapshot(): Readonly<IRoomSessionSnapshot> | null
Returns referentially-stable values: the same object reference is
returned across reads until invalidated. Invalidation happens via the
new event types NitroEventType.SESSION_DATA_UPDATED and
NitroEventType.ROOM_SESSION_UPDATED.
When you mutate any field that the snapshot exposes, call the private
invalidateUserDataSnapshot() / invalidateRoomSessionSnapshot() —
that drops the cached snapshot and dispatches the invalidation event.
The React side rebuilds via useSyncExternalStore.
The interface contracts live in:
packages/api/src/nitro/session/IUserDataSnapshot.tspackages/api/src/nitro/session/IRoomSessionSnapshot.ts
Recent renderer changes (feat/react19-event-bus)
Tracked separately from the v2.1.0 batch above; all are non-breaking additions or align-with-Arcturus fixes:
RoomEnterComposer: optional spawnX / spawnY
new RoomEnterComposer(roomId, password?, spawnX?, spawnY?). The
Arcturus RequestRoomLoadEvent handler reads the two extra ints only
when packet.remaining >= 8, so the same composer header serves both
the legacy 2-arg form (door spawn) and the 4-arg form (reconnect /
respawn at a specific tile). RoomSession + RoomSessionManager use the
4-arg variant in their enterRoom / reconnect paths.
RoomSettingsData: allowUnderpass field
RoomSettingsData (and its parser) now exposes allowUnderpass: boolean. Arcturus' RoomSettingsComposer already appends one
trailing int for this flag, and the new parser reads it via
if(wrapper.bytesAvailable) … readInt() === 1 so older servers that
don't emit the field still parse cleanly. SaveRoomSettingsComposer
accepts an optional allowUnderpass arg at the end of its parameter
list; the server-side RoomSettingsSaveEvent reads it under
packet.bytesAvailable() > 0.
Dropped dead code: sendWhisperGroupMessage
IRoomSession.sendWhisperGroupMessage(userId) referenced a
ChatWhisperGroupComposer that never existed in the codebase and had
zero call sites in the React client. Both the interface declaration
and the broken impl are removed. The real whisper path is
RoomUnitChatWhisperComposer(recipientName, message, styleId) —
unchanged.
TS 5.7+ and Pixi v8 alignment
ArrayBufferLikedrift handled with explicit casts inBinaryReader/BinaryWriter/WsSessionCrypto.randomNonce()/ArrayBufferToBase64. The renderer never uses SharedArrayBuffer, so these are type-level narrowings only.Container.filtersin Pixi v8 isFilter[] | readonly Filter[] | null; the AvatarImage filter-stack mutation always goes through the spread-array branch now (no single-Filter fallback).Filteris imported explicitly from pixi.js.ExtendedSpritecasts the renderer toWebGLRendererinside theRendererType.WEBGLbranch sorenderer.gl/glRenderTarget.resolveTargetFramebufferresolve.FurnitureBadgeDisplayVisualization.updateSpritesignature realigned to the parent's 2-arg(scale, layerId)shape (was a custom 4-arg override that broke base-class assignability).TextureUtils.generateImagecasts the extractor'sImageLikeunion return toHTMLImageElement(the default backend produces one).Window.NitroConfigdeclaration inNitroConfig.tsrealigned to the client'sRecord<string, unknown>type so the merged decls agree.- Empty-tuple composers (
WiredRoomSettingsRequestComposer,WiredUserVariablesRequestComposer) annotate the return type(): []explicitly soIMessageComposer<[]>lines up.
Bug fix: PetBreedingMessageParser.bytesAvailable < 12
bytesAvailable is a boolean (the wrapper just answers "is there
anything left?"). The pet-breeding parser used to compare it against
12 as if it were a byte count, which TS 6 caught and which was
also semantically wrong. Replaced with the standard
if(!wrapper || !wrapper.bytesAvailable) return false; guard.
Scripts
yarn build # vite build
yarn compile # tsc --project ./tsconfig.json --noEmit false
yarn compile:fast # tsgo (~7× faster, TS 7 preview)
yarn eslint # lint src + packages/*/src
yarn test # vitest run
yarn test:watch # vitest watch
yarn test:coverage # vitest with v8 coverage
Consumed by
../Nitro-V3 consumes this library via link:../Nitro_Render_V3
(yarn 4 node-modules linker). DO NOT use yarn link — it confuses
vite's asset resolution. The client's vite.config.js then maps each
@nitrots/* package directly to its source index.ts so there's no
build step needed for development.
When making changes to renderer APIs the React client uses, the
client's feat/react19-* branches contain consumers — check
Nitro-V3/src/hooks/events/ and Nitro-V3/src/hooks/{session,rooms}/
for the React-side bridge code.
Gotchas
SessionDataManager.getUserData(id)does NOT exist. Some legacy code in the React client used it under agetUserData ?truthy guard; the branch was always dead. OnlygetUserDataSnapshot()exists.bytesAvailableis a boolean. The codebase historically had one parser (PetBreedingMessageParser) that compared it against a number — fixed. The wrapper returns "any bytes left?", not a count. Use it as a truthy guard or follow withtry {} catchif you need optional reads.- Composer
getMessageArray()return type must match the type argument.IMessageComposer<[]>means the function returns[], notany[]. The twoWired*RequestComposers that ship empty payloads each annotategetMessageArray(): []explicitly. IRoomSession.sendChatMessage/sendShoutMessageaccept an optionalchatColour3rd arg (was required pre-2.1.1, now optional to match the historical call sites in the React client). The implementation forwardsundefinedto the composer just fine; pass a value only when you need a specific bubble colour.IRoomSession.passwordandIRoomSession.sendBackgroundMessageare now part of the public interface (they always existed on the implementation class — interface caught up).- The renderer is synchronous:
EventDispatcher.dispatchEventis a synchronous loop over listeners. Don't addawaitinside theprocessEventloop — it would change ordering guarantees that consumers rely on. - Workspace package devDeps pin TS at
^6.0.3soyarn compileinside any single package keeps working. The root TS 6 is the source of truth.
Sister projects in the same DEV folder
../Nitro-V3— React 19 client (consumes this lib via link)../Arcturus-Morningstar-Extended— Java emulator (server side)../NitroV3-Housekeeping— Next.js + Prisma admin CMS