1. Floor material: APPLY_COMPONENT_MATERIALS now directly updates the
specific component MeshObject's textureID instead of going through
room-level WallpaperId (which was overwritten/reset on each change).
2. Stairwell doorway: Reverted HasStairs check — stairwell rooms connect
horizontally through walls like any room. The doorway should open.
3. Added GetRoomComponentOptionID/GetHouseThemeID accessors to MeshObject
for per-component texture updates without room-level state.
Stairwell rooms are tall rooms (geobox Z=-1 to 14) placed ADJACENT to
the source room on the SAME floor, not above it. They extend vertically
through the ceiling. Only rooms connected at the stairwell's ceiling
door should go to floor+1. Was incorrectly using FloorIndex+1.
Interior decor was sent TWICE at login: once by the map visibility system
(AddToMap → UpdateObjectVisibilityOnCreate) and again by the deferred
callback (manual BuildCreateUpdateBlockForPlayer). The double CREATE
corrupted the client's entity state, making decor unselectable.
Session-placed decor only got one CREATE (via AddToMap) and worked fine.
Fix: removed the manual decor CREATE from the deferred callback. The
visibility system already handles decor delivery to the player.
1. APPLY_COMPONENT_MATERIALS handler now passes componentIDs filter to
UpdateRoomComponentVisuals (was updating ALL components including floor).
2. Stairwell room placement no longer destroys the source room's wall
(stairwells are vertical connections, not horizontal doorways).
Decor placed before the room entity system had empty RoomGuid in DB.
Without a valid AttachParentGUID (Housing/2 room entity), the client
can't select the decor for moving/removing. Now auto-assigns decor with
empty RoomGuid to the first non-base room. Also uses FloorIndex for
room Z position in decor world coordinate calculation.
The CMSG sends TextureID=4294967295 (0xFFFFFFFF) to mean "reset to
default". This was stored as WallpaperId and on respawn overrode the
texture lookup (WallpaperId != 0 → use -1 as texture). The client
showed an empty Floor Material dropdown because RoomComponentTextureID=-1.
Fix: treat WallpaperId=0xFFFFFFFF as 0 (no override) in both storage
and the spawn/update texture lookups. Also cleaned up -1 values in DB.
Note: RoomComponentOptionTexture DB2 table is empty in retail — Blizzard
hasn't populated it. Textures work via RoomComponentTexture (19 entries)
which the client reads directly by component type and theme.
1. Stairwell Z: Used room's own Height (14.0 for stairwells) instead of
standard floor height (7.0). Stairwells spawned at Z=14.1 instead of
7.1. Now uses fixed FLOOR_HEIGHT=7.0 for consistent floor spacing.
2. Dye wall/floor crossover: UpdateRoomComponentVisuals applied the theme
to ALL components. Now filters by the CMSG's RoomComponentIDs — only
the specified components are updated. Also uses the CMSG's theme ID
directly instead of room.ThemeId (which is room-level, not per-component).
Rooms now have FloorIndex (0=ground, 1+=upper floors) persisted to DB.
Room Z position is calculated from FloorIndex * RoomWmoData.Height
(typically 7 yards per floor). Stairwell rooms (HasStairs flag) are
placed on FloorIndex+1 of the source room.
Changes:
- Room struct: added FloorIndex field
- DB: added floorIndex column to character_housing_rooms
- SpawnRoomMeshObjects: Z = originZ + FloorIndex * ceilingHeight
- HousingRoomEntity: FloorIndex set from room data (was hardcoded 0)
- HandleHousingRoomAdd: stairwell rooms go to FloorIndex+1
- SELECT/INSERT/UPDATE queries updated for floorIndex column
- Client door filtering uses FloorIndex to show/hide stair connections:
Floor 0: hides downward stairs, Floor >=100: hides upward stairs
1. Dye: Sub-themes (e.g., 20=Folk Light) had no DB2 option entries.
Added GetBaseThemeID() to convert sub-theme→base theme for lookup.
Entity field gets the selected sub-theme, not the base.
2. Door GO teleport: Used cached _sourcePlotIndex=0. Now reads player's
housing data directly (GetPlotIndex, GetNeighborhoodGuid) to find
the correct exterior plot position.
3. Decor selection: Interior decor attaches to HousingRoomEntity (Housing
GUID) instead of a MeshObject. Selection may require further work.
LoadFromDB migration replaced any non-Room-1 visual room with Room 1,
destroying user-placed rooms (Stairwell, Hallway, etc.) and resetting
gridX/gridY to 0 — causing rooms to overlap. Users can place ANY room
type as their visual room. Migration now only adds a default Room 1 if
the house has NO non-base rooms at all (empty house).
The client's placement state machine needs SMSG_HOUSING_DECOR_PLACE_RESPONSE
to finalize the current placement before receiving the UPDATE_OBJECT with the
new MeshObject. Wrong order (CREATE before RESPONSE) corrupted the state on
repeated placements, causing the preview to snap to camera ("flies to camera").
Packet order now: 1) PLACE_RESPONSE, 2) MeshObject CREATE, 3) Account update.
The interior exit door used entry 586576 (exterior door, displayId=116973)
which is invisible inside the interior. Retail uses entry 575017 with
displayId=113554 (standalone interior door model). Created GO template
with correct display, flags=0x40000, owner=HouseGUID.
1. Interior door GO: The go_housing_door script only handled exterior
(HousingMap) clicks. When clicked from interior (HouseInteriorMap),
dynamic_cast failed → door did nothing. Now detects interior map and
teleports player back to the neighborhood at the plot position.
2. Room rotation: Updates HousingRoomEntity orientation in-place via
SetMirroredPosition() UPDATE_OBJECT instead of RefreshInteriorRoomVisuals.
Room rotation now updates the HousingRoomEntity's FMirroredPositionData_C
orientation via SetMirroredPosition() instead of RefreshInteriorRoomVisuals
(which crashes). The client receives an UPDATE_OBJECT with the new rotation
quaternion and applies it visually without destroy+create.
Also added GetOriginX/Y/Z accessors to HouseInteriorMap.
HousingPlayerHouseEntity and HousingNeighborhoodMirrorEntity are
unique_ptr on WorldSession. Dereferencing them without null checks
crashes on fresh characters or edge cases where the session constructor
failed to initialize them. Added HasHousing*Entity() checks before
AddToWorld/RemoveFromWorld in Player::AddToWorld/RemoveFromWorld.
Interior decor used the first MeshObject GUID from _roomMeshObjects as
AttachParentGUID. After removing the root room MeshObject, this pointed
to a component MeshObject which the client couldn't resolve as a valid
parent (NULL+0x08). Sniff-verified: retail decor attaches to the
HousingRoomEntity (Housing/2 GUID), not a MeshObject.
When a room is added at a door pin, the source room's wall at that
connection was still showing as a solid Cosmetic wall (spawned before
the connection existed). Now ReplaceWallWithDoorway() destroys the
Cosmetic MeshObject and spawns DoorwayWall+Doorway MeshObjects in its
place, creating a visible passage between the rooms immediately.
Stairwell walls have ConnectionType=0, so the door offset lookup found no
opposite wall and defaulted to 0 → room placed at door position instead of
at the correct distance. Now searches for ANY wall in the opposite direction
(the furthest boundary) regardless of ConnectionType.
Also removed RefreshInteriorRoomVisuals from Rotate/Move/DoorType/CeilingType
handlers to prevent same-GUID DESTROY+CREATE crash.
RefreshInteriorRoomVisuals (destroy+create ALL rooms) crashes the client
with same-GUID DESTROY+CREATE in one packet. Removed from Rotate, Move,
SetDoorType, SetCeilingType handlers. These operations change metadata
only — the client handles visual updates from the response packet.
TODO: implement proper in-place UPDATE_OBJECT for room entity changes
(rotation, door type, ceiling type) instead of full visual refresh.
Door adjacency checks used gridKey(GridX+1) which was correct for grid
indices but wrong for yard offsets. Entry at GridX=0 and Room1 at
GridX=15 are 15 yards apart, not 1 grid cell. Now uses findNeighborAtDoor
which searches all rooms in the door's facing direction instead of
looking for exact +1/-1 grid neighbors.