diff --git a/sql/updates/characters/master/2026_04_08_01_characters.sql.sql b/sql/updates/characters/master/2026_04_08_01_characters.sql.sql new file mode 100644 index 000000000..2430c7b3f --- /dev/null +++ b/sql/updates/characters/master/2026_04_08_01_characters.sql.sql @@ -0,0 +1 @@ +ALTER TABLE `character_housing_rooms` ADD COLUMN IF NOT EXISTS `floorIndex` INT NOT NULL DEFAULT 0 AFTER `gridY`; \ No newline at end of file diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index 291b1cbb6..07b3ee633 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -830,8 +830,8 @@ void CharacterDatabaseConnection::DoPrepareStatements() PrepareStatement(CHAR_SEL_CHARACTER_HOUSING_DECOR, "SELECT id, houseDecorId, posX, posY, posZ, rotX, rotY, rotZ, rotW, dyeSlot0, dyeSlot1, dyeSlot2, roomGuid, locked, placementTime, sourceType, sourceValue FROM character_housing_decor WHERE ownerGuid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_INS_CHARACTER_HOUSING_DECOR, "INSERT INTO character_housing_decor (ownerGuid, id, houseDecorId, posX, posY, posZ, rotX, rotY, rotZ, rotW, dyeSlot0, dyeSlot1, dyeSlot2, roomGuid, locked, placementTime, sourceType, sourceValue) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); PrepareStatement(CHAR_DEL_CHARACTER_HOUSING_DECOR, "DELETE FROM character_housing_decor WHERE ownerGuid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_SEL_CHARACTER_HOUSING_ROOMS, "SELECT id, houseRoomId, slotIndex, gridX, gridY, orientation, mirrored, themeId, wallpaperId, materialId, doorTypeId, doorSlot, ceilingTypeId, ceilingSlot FROM character_housing_rooms WHERE ownerGuid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_INS_CHARACTER_HOUSING_ROOMS, "INSERT INTO character_housing_rooms (ownerGuid, id, houseRoomId, slotIndex, gridX, gridY, orientation, mirrored, themeId, wallpaperId, materialId, doorTypeId, doorSlot, ceilingTypeId, ceilingSlot) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_CHARACTER_HOUSING_ROOMS, "SELECT id, houseRoomId, slotIndex, gridX, gridY, floorIndex, orientation, mirrored, themeId, wallpaperId, materialId, doorTypeId, doorSlot, ceilingTypeId, ceilingSlot FROM character_housing_rooms WHERE ownerGuid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_INS_CHARACTER_HOUSING_ROOMS, "INSERT INTO character_housing_rooms (ownerGuid, id, houseRoomId, slotIndex, gridX, gridY, floorIndex, orientation, mirrored, themeId, wallpaperId, materialId, doorTypeId, doorSlot, ceilingTypeId, ceilingSlot) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); PrepareStatement(CHAR_DEL_CHARACTER_HOUSING_ROOMS, "DELETE FROM character_housing_rooms WHERE ownerGuid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_CHARACTER_HOUSING_FIXTURES, "SELECT fixturePointId, fixtureOptionId FROM character_housing_fixtures WHERE ownerGuid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_INS_CHARACTER_HOUSING_FIXTURES, "INSERT INTO character_housing_fixtures (ownerGuid, fixturePointId, fixtureOptionId) VALUES (?, ?, ?)", CONNECTION_ASYNC); @@ -847,7 +847,7 @@ void CharacterDatabaseConnection::DoPrepareStatements() PrepareStatement(CHAR_UPD_CHARACTER_HOUSING_DECOR_POSITION, "UPDATE character_housing_decor SET posX = ?, posY = ?, posZ = ?, rotX = ?, rotY = ?, rotZ = ?, rotW = ? WHERE ownerGuid = ? AND id = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_UPD_CHARACTER_HOUSING_DECOR_DYES, "UPDATE character_housing_decor SET dyeSlot0 = ?, dyeSlot1 = ?, dyeSlot2 = ? WHERE ownerGuid = ? AND id = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_UPD_CHARACTER_HOUSING_DECOR_LOCKED, "UPDATE character_housing_decor SET locked = ? WHERE ownerGuid = ? AND id = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_UPD_CHARACTER_HOUSING_ROOM, "UPDATE character_housing_rooms SET slotIndex = ?, gridX = ?, gridY = ?, orientation = ?, mirrored = ?, themeId = ?, wallpaperId = ?, materialId = ?, doorTypeId = ?, doorSlot = ?, ceilingTypeId = ?, ceilingSlot = ? WHERE ownerGuid = ? AND id = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_UPD_CHARACTER_HOUSING_ROOM, "UPDATE character_housing_rooms SET slotIndex = ?, gridX = ?, gridY = ?, floorIndex = ?, orientation = ?, mirrored = ?, themeId = ?, wallpaperId = ?, materialId = ?, doorTypeId = ?, doorSlot = ?, ceilingTypeId = ?, ceilingSlot = ? WHERE ownerGuid = ? AND id = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_UPD_CHARACTER_HOUSING_FIXTURE, "UPDATE character_housing_fixtures SET fixtureOptionId = ? WHERE ownerGuid = ? AND fixturePointId = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_DEL_CHARACTER_HOUSING_FIXTURE_SINGLE, "DELETE FROM character_housing_fixtures WHERE ownerGuid = ? AND fixturePointId = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_REP_CHARACTER_HOUSING_CATALOG, "REPLACE INTO character_housing_catalog (ownerGuid, houseDecorId, quantity, sourceType, sourceValue) VALUES (?, ?, ?, ?, ?)", CONNECTION_ASYNC); diff --git a/src/server/game/Handlers/HousingHandler.cpp b/src/server/game/Handlers/HousingHandler.cpp index ab150b60c..e3529f406 100644 --- a/src/server/game/Handlers/HousingHandler.cpp +++ b/src/server/game/Handlers/HousingHandler.cpp @@ -2282,6 +2282,7 @@ void WorldSession::HandleHousingRoomAdd(WorldPackets::Housing::HousingRoomAdd co // The CMSG sends TargetDoorComponentID ? find which room owns this door, // determine the door's direction, and compute the 2D grid position for the new room. int32 newGridX = 0, newGridY = 0; + int32 newGridX = 0, newGridY = 0, newFloorIndex = 0; uint32 nextSlot = 0; { // Find the source room that owns the target door component @@ -2358,6 +2359,19 @@ void WorldSession::HandleHousingRoomAdd(WorldPackets::Housing::HousingRoomAdd co newGridX = room.GridX; newGridY = room.GridY + static_cast(sourceDoorOffset - newDoorOffset); } + // Check if the new room is a stairwell ? place on a different floor + HouseRoomData const* newRoomData = sHousingMgr.GetHouseRoomData(housingRoomAdd.HouseRoomID); + if (newRoomData && newRoomData->HasStairs()) + { + // Stairwell: same grid position as source, but FloorIndex+1 + newGridX = room.GridX; + newGridY = room.GridY; + newFloorIndex = room.FloorIndex + 1; + } + else + { + newFloorIndex = room.FloorIndex; // same floor as source + } goto foundDoor; } } @@ -2370,7 +2384,7 @@ void WorldSession::HandleHousingRoomAdd(WorldPackets::Housing::HousingRoomAdd co ObjectGuid newRoomGuid; HousingResult result = housing->PlaceRoom(housingRoomAdd.HouseRoomID, nextSlot, - /*orientation*/ 0, /*mirrored*/ false, &newRoomGuid, newGridX, newGridY); + /*orientation*/ 0, /*mirrored*/ false, &newRoomGuid, newGridX, newGridY, newFloorIndex); WorldPackets::Housing::HousingRoomAddResponse response; response.Result = static_cast(result); diff --git a/src/server/game/Housing/HouseInteriorMap.cpp b/src/server/game/Housing/HouseInteriorMap.cpp index 0853970a6..33dc025e6 100644 --- a/src/server/game/Housing/HouseInteriorMap.cpp +++ b/src/server/game/Housing/HouseInteriorMap.cpp @@ -208,11 +208,12 @@ void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing, int32 factionRestr geoMinX, geoMinY, geoMinZ, geoMaxX, geoMaxY, geoMaxZ); // --- Calculate room world position --- - // GridX/GridY store yard offsets from origin. Entry at (0,0), Room1 at (15,0). - // New rooms are positioned based on connecting door offsets (not fixed spacing). + // GridX/GridY = yard offsets. FloorIndex = vertical floor (0=ground, 1+=upper). + // Floor height from RoomWmoData.Height (typically 7.0 yards per floor). + float ceilingHeight = wmoData ? wmoData->Height : 7.0f; float roomX = _originX + static_cast(room->GridX); float roomY = _originY + static_cast(room->GridY); - float roomZ = _originZ; + float roomZ = _originZ + static_cast(room->FloorIndex) * ceilingHeight; float roomFacing = static_cast(room->Orientation) * (M_PI / 2.0f); Position roomPos(roomX, roomY, roomZ, roomFacing); @@ -229,7 +230,7 @@ void HouseInteriorMap::SpawnRoomMeshObjects(Housing* housing, int32 factionRestr GridMarkNoUnload(roomGrid.x_coord, roomGrid.y_coord); int32 roomFlags = roomData->IsBaseRoom() ? 1 : 0; - int32 floorIndex = 0; + int32 floorIndex = room->FloorIndex; ObjectGuid roomHousingGuid = room->Guid; // Housing/2 GUID for attach parent // --- Phase 1: Create HousingRoomEntity FIRST --- diff --git a/src/server/game/Housing/Housing.cpp b/src/server/game/Housing/Housing.cpp index 549faca5b..ef5333873 100644 --- a/src/server/game/Housing/Housing.cpp +++ b/src/server/game/Housing/Housing.cpp @@ -34,9 +34,9 @@ #include #include -// Global DB ID generators ? initialized from MAX(id) at server startup -std::atomic Housing::s_nextDecorDbId{1}; -std::atomic Housing::s_nextRoomDbId{1}; + // Global DB ID generators ? initialized from MAX(id) at server startup +std::atomic Housing::s_nextDecorDbId{ 1 }; +std::atomic Housing::s_nextRoomDbId{ 1 }; Housing::Housing(Player* owner) : _owner(owner) @@ -101,8 +101,8 @@ bool Housing::LoadFromDB(PreparedQueryResult housing, PreparedQueryResult decor, _houseDescription = fields[15].GetString(); // Load rooms FIRST so decor can look up roomEntryId for GUID arg2 - // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 - // SELECT roomGuid, roomEntryId, slotIndex, gridX, gridY, orientation, mirrored, themeId, wallpaperId, materialId, doorTypeId, doorSlot, ceilingTypeId, ceilingSlot + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 + // SELECT roomGuid, roomEntryId, slotIndex, gridX, gridY, floorIndex, orientation, mirrored, themeId, wallpaperId, materialId, doorTypeId, doorSlot, ceilingTypeId, ceilingSlot // FROM character_housing_rooms WHERE ownerGuid = ? if (rooms) { @@ -127,18 +127,19 @@ bool Housing::LoadFromDB(PreparedQueryResult housing, PreparedQueryResult decor, room.SlotIndex = fields[2].GetUInt32(); room.GridX = fields[3].GetInt32(); room.GridY = fields[4].GetInt32(); + room.FloorIndex = fields[5].GetInt32(); // Backward compat: if gridX looks like old grid index (0-20), convert to yards if (room.GridX >= 0 && room.GridX <= 20 && room.GridX == static_cast(room.SlotIndex) && room.SlotIndex > 0) - room.GridX = static_cast(room.SlotIndex) * 15; // old linear: slot*15 yards - room.Orientation = fields[5].GetUInt32(); - room.Mirrored = fields[6].GetBool(); - room.ThemeId = fields[7].GetUInt32(); - room.WallpaperId = fields[8].GetUInt32(); - room.MaterialId = fields[9].GetUInt32(); - room.DoorTypeId = fields[10].GetUInt32(); - room.DoorSlot = fields[11].GetUInt8(); - room.CeilingTypeId = fields[12].GetUInt32(); - room.CeilingSlot = fields[13].GetUInt8(); + room.GridX = static_cast(room.SlotIndex) * 15; + room.Orientation = fields[6].GetUInt32(); + room.Mirrored = fields[7].GetBool(); + room.ThemeId = fields[8].GetUInt32(); + room.WallpaperId = fields[9].GetUInt32(); + room.MaterialId = fields[10].GetUInt32(); + room.DoorTypeId = fields[11].GetUInt32(); + room.DoorSlot = fields[12].GetUInt8(); + room.CeilingTypeId = fields[13].GetUInt32(); + room.CeilingSlot = fields[14].GetUInt8(); // Advance global generator if needed (safety net) uint64 expected = s_nextRoomDbId.load(); @@ -215,11 +216,10 @@ bool Housing::LoadFromDB(PreparedQueryResult housing, PreparedQueryResult decor, hasVisualRoom = true; // Multiple visual rooms ? don't mess with them } - // NOTE: Previously replaced non-Room-1 rooms with Room 1. This was too aggressive ? - // it replaced valid user placements (e.g., Stairwell) and lost gridX/gridY coordinates. - // Rooms placed by the user are valid regardless of entry type. Only add a default - // visual room if there are NO non-base rooms at all (empty house). - + // NOTE: Previously replaced non-Room-1 rooms with Room 1. This was too aggressive ? + // it replaced valid user placements (e.g., Stairwell) and lost gridX/gridY coordinates. + // Rooms placed by the user are valid regardless of entry type. Only add a default + // visual room if there are NO non-base rooms at all (empty house). // No visual room at all ? add one if (!hasVisualRoom && wrongRoomGuid.IsEmpty() && correctVisualRoom) @@ -496,6 +496,7 @@ void Housing::SaveToDB(CharacterDatabaseTransaction trans) stmt->setUInt32(index++, room.SlotIndex); stmt->setInt32(index++, room.GridX); stmt->setInt32(index++, room.GridY); + stmt->setInt32(index++, room.FloorIndex); stmt->setUInt32(index++, room.Orientation); stmt->setBool(index++, room.Mirrored); stmt->setUInt32(index++, room.ThemeId); @@ -1285,7 +1286,7 @@ std::vector Housing::GetAllPlacedDecor() const return result; } -HousingResult Housing::PlaceRoom(uint32 roomEntryId, uint32 slotIndex, uint32 orientation, bool mirrored, ObjectGuid* outRoomGuid, int32 gridX, int32 gridY) +HousingResult Housing::PlaceRoom(uint32 roomEntryId, uint32 slotIndex, uint32 orientation, bool mirrored, ObjectGuid* outRoomGuid, int32 gridX, int32 gridY, int32 floorIndex) { if (_houseGuid.IsEmpty()) return HOUSING_RESULT_HOUSE_NOT_FOUND; @@ -1350,6 +1351,7 @@ HousingResult Housing::PlaceRoom(uint32 roomEntryId, uint32 slotIndex, uint32 or room.SlotIndex = slotIndex; room.GridX = gridX; room.GridY = gridY; + room.FloorIndex = floorIndex; room.Orientation = orientation; room.Mirrored = mirrored; room.ThemeId = 0; @@ -2646,4 +2648,3 @@ void Housing::PopulateStarterFixtures() } } } - diff --git a/src/server/game/Housing/Housing.h b/src/server/game/Housing/Housing.h index 7e41d4d4e..4fd204a28 100644 --- a/src/server/game/Housing/Housing.h +++ b/src/server/game/Housing/Housing.h @@ -60,8 +60,9 @@ public: ObjectGuid Guid; uint32 RoomEntryId = 0; uint32 SlotIndex = 0; - int32 GridX = 0; // 2D grid position (rooms connect via doors) + int32 GridX = 0; // 2D grid position (yard offsets from origin) int32 GridY = 0; + int32 FloorIndex = 0; // 0=ground, 1+=upper floors uint32 Orientation = 0; bool Mirrored = false; uint32 ThemeId = 0; @@ -146,7 +147,7 @@ public: uint32 PlaceStarterDecor(); // Room operations - HousingResult PlaceRoom(uint32 roomEntryId, uint32 slotIndex, uint32 orientation, bool mirrored, ObjectGuid* outRoomGuid = nullptr, int32 gridX = 0, int32 gridY = 0); + HousingResult PlaceRoom(uint32 roomEntryId, uint32 slotIndex, uint32 orientation, bool mirrored, ObjectGuid* outRoomGuid = nullptr, int32 gridX = 0, int32 gridY = 0, int32 floorIndex = 0); HousingResult RemoveRoom(ObjectGuid roomGuid); HousingResult RotateRoom(ObjectGuid roomGuid, bool clockwise); HousingResult MoveRoom(ObjectGuid roomGuid, uint32 newSlotIndex, ObjectGuid swapRoomGuid, uint32 swapSlotIndex);