From afd0a4fa1687889e2350b349ffe2d4cc25d10983 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 18:50:01 +0200 Subject: [PATCH 1/4] 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. --- packages/room/src/RoomMessageHandler.ts | 76 ++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/packages/room/src/RoomMessageHandler.ts b/packages/room/src/RoomMessageHandler.ts index b97c6b6..634d6d6 100644 --- a/packages/room/src/RoomMessageHandler.ts +++ b/packages/room/src/RoomMessageHandler.ts @@ -2,11 +2,17 @@ import { AvatarGuideStatus, IConnection, IMessageEvent, IRoomCreator, IRoomObjec import { AreaHideMessageEvent, ConfInvisStateMessageEvent, DiceValueMessageEvent, FloorHeightMapEvent, FurnitureAliasesComposer, FurnitureAliasesEvent, FurnitureDataEvent, FurnitureFloorAddEvent, FurnitureFloorDataParser, FurnitureFloorEvent, FurnitureFloorRemoveEvent, FurnitureFloorUpdateEvent, FurnitureWallAddEvent, FurnitureWallDataParser, FurnitureWallEvent, FurnitureWallRemoveEvent, FurnitureWallUpdateEvent, GetCommunication, GetRoomEntryDataMessageComposer, GuideSessionEndedMessageEvent, GuideSessionErrorMessageEvent, GuideSessionStartedMessageEvent, IgnoreResultEvent, ItemDataUpdateMessageEvent, ObjectsDataUpdateEvent, ObjectsRollingEvent, OneWayDoorStatusMessageEvent, PetExperienceEvent, PetFigureUpdateEvent, RoomEntryTileMessageEvent, RoomEntryTileMessageParser, RoomHeightMapEvent, RoomHeightMapUpdateEvent, RoomPaintEvent, RoomReadyMessageEvent, RoomUnitChatEvent, RoomUnitChatShoutEvent, RoomUnitChatWhisperEvent, RoomUnitDanceEvent, RoomUnitEffectEvent, RoomUnitEvent, RoomUnitExpressionEvent, RoomUnitHandItemEvent, RoomUnitIdleEvent, RoomUnitInfoEvent, RoomUnitNumberEvent, RoomUnitRemoveEvent, RoomUnitStatusEvent, RoomUnitStatusMessage, RoomUnitTypingEvent, RoomVisualizationSettingsEvent, UserInfoEvent, WiredFurniMovementData, WiredMovementsEvent, WiredUserDirectionUpdateData, WiredUserMovementData, YouArePlayingGameEvent } from '@nitrots/communication'; import { GetRoomSessionManager, GetSessionDataManager } from '@nitrots/session'; import { Vector3d } from '@nitrots/utils'; +import { FloorHeightMapMessageParser } from '@nitrots/communication'; import { GetRoomEngine } from './GetRoomEngine'; import { RoomVariableEnum } from './RoomVariableEnum'; +import { ObjectRoomMapUpdateMessage } from './messages'; import { RoomPlaneParser } from './object/RoomPlaneParser'; import { FurnitureStackingHeightMap, LegacyWallGeometry } from './utils'; +// Local mirror of `RoomEngine.ROOM_OBJECT_ID` to avoid a circular +// import between this handler and the engine that owns it. +const ROOM_OWN_OBJECT_ID = -1; + type AreaHideControllerState = { rootX: number; rootY: number; @@ -232,9 +238,75 @@ export class RoomMessageHandler if(!parser) return; + const roomMap = this._rebuildFloorGeometry(parser); + + if(!roomMap) return; + + // Initial server-driven load: create the instance from + // scratch. RoomEngine.createRoomInstance is a no-op when + // the room already exists, so we never accidentally wipe + // furniture/avatars on a re-enter. + this._roomEngine.createRoomInstance(this._currentRoomId, roomMap); + } + + /** + * Apply a floor model to the ACTIVE room locally, without + * touching the server. Drives the same `wallGeometry` + + * `RoomPlaneParser` pipeline as the wire-driven path, then + * routes the resulting `RoomMapData` through the room object's + * `ObjectRoomMapUpdateMessage` channel — the same mechanism + * `RoomPreviewer.updateRoomPlanes` uses. The visualization + * rebuilds in place, so existing furniture and avatars are + * preserved. + * + * Intended for tools that need a live in-room preview of a + * floor edit before the user commits to a server save (e.g. + * the React floor-plan editor's live-preview mode). The wire + * `UpdateFloorPropertiesMessageComposer` is still the source + * of truth — call this purely for transient client-side + * preview, then send the composer separately when the user + * confirms. + * + * @returns `true` if the floor was rebuilt; `false` if no + * active room is bound, the engine isn't ready, or the + * model string failed to parse. + */ + public applyFloorModelLocally(modelString: string, wallHeight: number, scale: boolean = true): boolean + { + if(!this._roomEngine || this._currentRoomId <= 0 || !modelString) return false; + + const parser = new FloorHeightMapMessageParser(); + + parser.flush(); + + if(!parser.parseModel(modelString, wallHeight, scale)) return false; + + const roomMap = this._rebuildFloorGeometry(parser); + + if(!roomMap) return false; + + const roomObject = this._roomEngine.getRoomObject(this._currentRoomId, ROOM_OWN_OBJECT_ID, RoomObjectCategory.ROOM); + + if(!roomObject) return false; + + roomObject.processUpdateMessage(new ObjectRoomMapUpdateMessage(roomMap)); + + return true; + } + + /** + * Shared body of `onRoomModelEvent` and + * `applyFloorModelLocally`. Feeds the floor heightmap into + * `_planeParser`, refreshes `wallGeometry`, and returns the + * resulting `RoomMapData` (doors included). The caller decides + * whether to seed a fresh room (initial enter) or update an + * existing one (live preview). + */ + private _rebuildFloorGeometry(parser: FloorHeightMapMessageParser) + { const wallGeometry = this._roomEngine.getLegacyWallGeometry(this._currentRoomId); - if(!wallGeometry) return; + if(!wallGeometry) return null; this._planeParser.reset(); @@ -320,7 +392,7 @@ export class RoomMessageHandler dir: doorDirection }); - this._roomEngine.createRoomInstance(this._currentRoomId, roomMap); + return roomMap; } private onRoomHeightMapEvent(event: RoomHeightMapEvent): void From 28a41ba54342305057ca13daeccf9fd4eb2593b0 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 19:16:40 +0200 Subject: [PATCH 2/4] 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. --- packages/room/src/RoomMessageHandler.ts | 42 +++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/room/src/RoomMessageHandler.ts b/packages/room/src/RoomMessageHandler.ts index 634d6d6..9354d0c 100644 --- a/packages/room/src/RoomMessageHandler.ts +++ b/packages/room/src/RoomMessageHandler.ts @@ -291,9 +291,51 @@ export class RoomMessageHandler roomObject.processUpdateMessage(new ObjectRoomMapUpdateMessage(roomMap)); + // Floor visualization is updated above. Without this second + // step the FurnitureStackingHeightMap still reflects the + // pre-edit floor, so the room thinks every newly-painted + // tile is "blocked" and rejects furni placement on it. + // Rebuild it from the same parser so the stacking-map and + // the visual floor agree. + this._rebuildFurnitureStackingMap(parser); + return true; } + private _rebuildFurnitureStackingMap(parser: FloorHeightMapMessageParser): void + { + if(!this._roomEngine) return; + + const width = parser.width; + const height = parser.height; + const heightMap = new FurnitureStackingHeightMap(width, height); + const BLOCKED = FloorHeightMapMessageParser.TILE_BLOCKED; + + let y = 0; + + while(y < height) + { + let x = 0; + + while(x < width) + { + const tileHeight = parser.getHeight(x, y); + const isRoomTile = (tileHeight !== BLOCKED); + + heightMap.setTileHeight(x, y, isRoomTile ? tileHeight : 0); + heightMap.setStackingBlocked(x, y, false); + heightMap.setIsRoomTile(x, y, isRoomTile); + + x++; + } + + y++; + } + + this._roomEngine.setFurnitureStackingHeightMap(this._currentRoomId, heightMap); + this._roomEngine.refreshTileObjectMap(this._currentRoomId, 'RoomMessageHandler.applyFloorModelLocally'); + } + /** * Shared body of `onRoomModelEvent` and * `applyFloorModelLocally`. Feeds the floor heightmap into From 2504aea85f07403708cf443fef2f9a3b909b4133 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 20:43:50 +0200 Subject: [PATCH 3/4] 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. --- packages/room/src/RoomPreviewer.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/room/src/RoomPreviewer.ts b/packages/room/src/RoomPreviewer.ts index 53b7537..67e9677 100644 --- a/packages/room/src/RoomPreviewer.ts +++ b/packages/room/src/RoomPreviewer.ts @@ -136,6 +136,12 @@ export class RoomPreviewer public updatePreviewModel(model: string, wallHeight: number, scale: boolean = true): void { + // Defensive: dispose() nulls _planeParser, and React 19 + // StrictMode dev double-mount can leave a stale reference + // briefly pointing at a disposed instance. Bail rather + // than crashing with "cannot read property reset of null". + if(!this._planeParser) return; + const parser = new FloorHeightMapMessageParser(); parser.flush(); From b7688f9d2b05121720720fe94e02586db8598411 Mon Sep 17 00:00:00 2001 From: duckietm Date: Wed, 27 May 2026 09:41:18 +0200 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=86=95=20Added=20Pickup=20furni=20to?= =?UTF-8?q?=20the=20floorplan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UpdateFloorPropertiesMessageComposer.ts | 4 +- packages/room/src/RoomMessageHandler.ts | 96 ++----------------- 2 files changed, 12 insertions(+), 88 deletions(-) diff --git a/packages/communication/src/messages/outgoing/room/layout/UpdateFloorPropertiesMessageComposer.ts b/packages/communication/src/messages/outgoing/room/layout/UpdateFloorPropertiesMessageComposer.ts index 44b48d3..13b9509 100644 --- a/packages/communication/src/messages/outgoing/room/layout/UpdateFloorPropertiesMessageComposer.ts +++ b/packages/communication/src/messages/outgoing/room/layout/UpdateFloorPropertiesMessageComposer.ts @@ -4,9 +4,9 @@ export class UpdateFloorPropertiesMessageComposer implements IMessageComposer; - constructor(model: string, doorX: number, doorY: number, doorDirection: number, thicknessWall: number, thicknessFloor: number, wallHeight: number) + constructor(model: string, doorX: number, doorY: number, doorDirection: number, thicknessWall: number, thicknessFloor: number, wallHeight: number, autoPickup: boolean = false) { - this._data = [model, doorX, doorY, doorDirection, thicknessWall, thicknessFloor, wallHeight]; + this._data = [model, doorX, doorY, doorDirection, thicknessWall, thicknessFloor, wallHeight, autoPickup]; } public getMessageArray() diff --git a/packages/room/src/RoomMessageHandler.ts b/packages/room/src/RoomMessageHandler.ts index 9354d0c..bdfe065 100644 --- a/packages/room/src/RoomMessageHandler.ts +++ b/packages/room/src/RoomMessageHandler.ts @@ -9,8 +9,6 @@ import { ObjectRoomMapUpdateMessage } from './messages'; import { RoomPlaneParser } from './object/RoomPlaneParser'; import { FurnitureStackingHeightMap, LegacyWallGeometry } from './utils'; -// Local mirror of `RoomEngine.ROOM_OBJECT_ID` to avoid a circular -// import between this handler and the engine that owns it. const ROOM_OWN_OBJECT_ID = -1; type AreaHideControllerState = { @@ -56,8 +54,6 @@ export class RoomMessageHandler { this._connection = GetCommunication().connection; this._roomEngine = GetRoomEngine(); - - // Store all message events for cleanup this._messageEvents = [ new UserInfoEvent(this.onUserInfoEvent.bind(this)), new RoomReadyMessageEvent(this.onRoomReadyMessageEvent.bind(this)), @@ -108,7 +104,6 @@ export class RoomMessageHandler new GuideSessionErrorMessageEvent(this.onGuideSessionErrorMessageEvent.bind(this)) ]; - // Register all message events for(const event of this._messageEvents) { this._connection.addMessageEvent(event); @@ -117,7 +112,6 @@ export class RoomMessageHandler public dispose(): void { - // Remove all message events if(this._connection) { for(const event of this._messageEvents) @@ -242,35 +236,9 @@ export class RoomMessageHandler if(!roomMap) return; - // Initial server-driven load: create the instance from - // scratch. RoomEngine.createRoomInstance is a no-op when - // the room already exists, so we never accidentally wipe - // furniture/avatars on a re-enter. this._roomEngine.createRoomInstance(this._currentRoomId, roomMap); } - /** - * Apply a floor model to the ACTIVE room locally, without - * touching the server. Drives the same `wallGeometry` + - * `RoomPlaneParser` pipeline as the wire-driven path, then - * routes the resulting `RoomMapData` through the room object's - * `ObjectRoomMapUpdateMessage` channel — the same mechanism - * `RoomPreviewer.updateRoomPlanes` uses. The visualization - * rebuilds in place, so existing furniture and avatars are - * preserved. - * - * Intended for tools that need a live in-room preview of a - * floor edit before the user commits to a server save (e.g. - * the React floor-plan editor's live-preview mode). The wire - * `UpdateFloorPropertiesMessageComposer` is still the source - * of truth — call this purely for transient client-side - * preview, then send the composer separately when the user - * confirms. - * - * @returns `true` if the floor was rebuilt; `false` if no - * active room is bound, the engine isn't ready, or the - * model string failed to parse. - */ public applyFloorModelLocally(modelString: string, wallHeight: number, scale: boolean = true): boolean { if(!this._roomEngine || this._currentRoomId <= 0 || !modelString) return false; @@ -289,16 +257,18 @@ export class RoomMessageHandler if(!roomObject) return false; + const currentMap = roomObject.model.getValue<{ holeMap?: { id: number, x: number, y: number, width: number, height: number, invert: boolean }[] }>(RoomObjectVariable.ROOM_MAP_DATA); + + if(currentMap && currentMap.holeMap && currentMap.holeMap.length) + { + for(const hole of currentMap.holeMap) + { + if(hole) roomMap.holeMap.push(hole); + } + } + roomObject.processUpdateMessage(new ObjectRoomMapUpdateMessage(roomMap)); - - // Floor visualization is updated above. Without this second - // step the FurnitureStackingHeightMap still reflects the - // pre-edit floor, so the room thinks every newly-painted - // tile is "blocked" and rejects furni placement on it. - // Rebuild it from the same parser so the stacking-map and - // the visual floor agree. this._rebuildFurnitureStackingMap(parser); - return true; } @@ -336,14 +306,6 @@ export class RoomMessageHandler this._roomEngine.refreshTileObjectMap(this._currentRoomId, 'RoomMessageHandler.applyFloorModelLocally'); } - /** - * Shared body of `onRoomModelEvent` and - * `applyFloorModelLocally`. Feeds the floor heightmap into - * `_planeParser`, refreshes `wallGeometry`, and returns the - * resulting `RoomMapData` (doors included). The caller decides - * whether to seed a fresh room (initial enter) or update an - * existing one (live preview). - */ private _rebuildFloorGeometry(parser: FloorHeightMapMessageParser) { const wallGeometry = this._roomEngine.getLegacyWallGeometry(this._currentRoomId); @@ -1798,44 +1760,6 @@ export class RoomMessageHandler this._roomEngine.updateRoomObjectUserAction(this._currentRoomId, userData.roomIndex, RoomObjectVariable.FIGURE_GUIDE_STATUS, status); } - // public _SafeStr_10580(event:_SafeStr_2242): void - // { - // var arrayIndex: number; - // var discoColours:Array; - // var discoTimer:Timer; - // var eventParser:_SafeStr_4576 = (event.parser as _SafeStr_4576); - // switch (eventParser._SafeStr_7025) - // { - // case 0: - // _SafeStr_4588.init(250, 5000); - // _SafeStr_4588._SafeStr_6766(); - // return; - // case 1: - // _SafeStr_4231.init(250, 5000); - // _SafeStr_4231._SafeStr_6766(); - // return; - // case 2: - // NitroEventDispatcher.dispatchEvent(new _SafeStr_2821(this._SafeStr_10593, -1, true)); - // return; - // case 3: - // arrayIndex = 0; - // discoColours = [29371, 16731195, 16764980, 0x99FF00, 29371, 16731195, 16764980, 0x99FF00, 0]; - // discoTimer = new Timer(1000, (discoColours.length + 1)); - // discoTimer.addEventListener(TimerEvent.TIMER, function (k:TimerEvent): void - // { - // if (arrayIndex == discoColours.length) - // { - // _SafeStr_10592._SafeStr_21164(_SafeStr_10593, discoColours[arrayIndex++], 176, true); - // } else - // { - // _SafeStr_10592._SafeStr_21164(_SafeStr_10593, discoColours[arrayIndex++], 176, false); - // }; - // }); - // discoTimer.start(); - // return; - // }; - // } - public get currentRoomId(): number { return this._currentRoomId;