Merge pull request #50 from duckietm/dev

🆙 Fix rooms loading
This commit is contained in:
DuckieTM
2026-03-26 15:19:23 +01:00
committed by GitHub
4 changed files with 180 additions and 97 deletions
@@ -174,7 +174,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
private volatile boolean muted; private volatile boolean muted;
private RoomSpecialTypes roomSpecialTypes; private RoomSpecialTypes roomSpecialTypes;
private TraxManager traxManager; private TraxManager traxManager;
public final THashMap<String, Object> cache; public final THashMap<String, Object> cache;
public Room(ResultSet set) throws SQLException { public Room(ResultSet set) throws SQLException {
@@ -221,22 +221,8 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
this.bannedHabbos = new TIntObjectHashMap<>(); this.bannedHabbos = new TIntObjectHashMap<>();
try (Connection connection = Emulator.getDatabase().getDataSource() try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
.getConnection(); PreparedStatement statement = connection.prepareStatement( // Load bans eagerly (needed for entry check before loadData)
"SELECT * FROM room_promotions WHERE room_id = ? AND end_timestamp > ? LIMIT 1")) {
if (this.promoted) {
statement.setInt(1, this.id);
statement.setInt(2, Emulator.getIntUnixTimestamp());
try (ResultSet promotionSet = statement.executeQuery()) {
this.promoted = false;
if (promotionSet.next()) {
this.promoted = true;
this.promotion = new RoomPromotion(this, promotionSet);
}
}
}
this.loadBans(connection); this.loadBans(connection);
} catch (SQLException e) { } catch (SQLException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
@@ -398,7 +384,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
if (this.loaded || this.loadingInProgress || !this.preLoaded) { if (this.loaded || this.loadingInProgress || !this.preLoaded) {
return; return;
} }
this.loadingInProgress = true; this.loadingInProgress = true;
this.loadingFuture = CompletableFuture.runAsync(() -> { this.loadingFuture = CompletableFuture.runAsync(() -> {
this.loadDataInternal(); this.loadDataInternal();
@@ -418,7 +404,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
future = this.loadingFuture; future = this.loadingFuture;
} }
if (future != null) { if (future != null) {
try { try {
future.join(); future.join();
@@ -433,7 +419,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
public void loadData() { public void loadData() {
CompletableFuture<Void> futureToWait = null; CompletableFuture<Void> futureToWait = null;
boolean shouldLoad = false; boolean shouldLoad = false;
synchronized (this.loadLock) { synchronized (this.loadLock) {
if (this.loadingInProgress) { if (this.loadingInProgress) {
// Get the future to wait on outside the lock // Get the future to wait on outside the lock
@@ -443,7 +429,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
shouldLoad = true; shouldLoad = true;
} }
} }
// Wait for existing load outside the lock // Wait for existing load outside the lock
if (futureToWait != null) { if (futureToWait != null) {
try { try {
@@ -453,7 +439,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
return; return;
} }
// Load if needed // Load if needed
if (shouldLoad) { if (shouldLoad) {
this.loadDataInternal(); this.loadDataInternal();
@@ -489,7 +475,26 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
LOGGER.error("Caught exception loading layout", e); LOGGER.error("Caught exception loading layout", e);
} }
// Phase 2: Load items and rights in parallel (independent operations) if (this.promoted) {
CompletableFuture.runAsync(() -> {
try (Connection promoConnection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = promoConnection.prepareStatement(
"SELECT * FROM room_promotions WHERE room_id = ? AND end_timestamp > ? LIMIT 1")) {
stmt.setInt(1, this.id);
stmt.setInt(2, Emulator.getIntUnixTimestamp());
try (ResultSet promoSet = stmt.executeQuery()) {
this.promoted = false;
if (promoSet.next()) {
this.promoted = true;
this.promotion = new RoomPromotion(this, promoSet);
}
}
} catch (Exception e) {
LOGGER.error("Caught exception loading promotion", e);
}
}, Emulator.getThreading().getService());
}
CompletableFuture<Void> itemsFuture = CompletableFuture.runAsync(() -> { CompletableFuture<Void> itemsFuture = CompletableFuture.runAsync(() -> {
try (Connection itemConnection = Emulator.getDatabase().getDataSource().getConnection()) { try (Connection itemConnection = Emulator.getDatabase().getDataSource().getConnection()) {
this.loadItems(itemConnection); this.loadItems(itemConnection);
@@ -514,21 +519,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
}, Emulator.getThreading().getService()); }, Emulator.getThreading().getService());
// Wait for items to be loaded before loading wired data (wired depends on items) // Bots and pets only need layout for positioning - start them now
try {
itemsFuture.join();
} catch (Exception e) {
LOGGER.error("Error waiting for items to load", e);
}
// Phase 3: Load heightmap after items are loaded (depends on items for stack heights)
try {
this.loadHeightmap();
} catch (Exception e) {
LOGGER.error("Caught exception loading heightmap", e);
}
// Phase 4: Load bots, pets, and wired data in parallel (all depend on layout + items)
CompletableFuture<Void> botsFuture = CompletableFuture.runAsync(() -> { CompletableFuture<Void> botsFuture = CompletableFuture.runAsync(() -> {
try (Connection botsConnection = Emulator.getDatabase().getDataSource().getConnection()) { try (Connection botsConnection = Emulator.getDatabase().getDataSource().getConnection()) {
this.loadBots(botsConnection); this.loadBots(botsConnection);
@@ -545,6 +536,22 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
}, Emulator.getThreading().getService()); }, Emulator.getThreading().getService());
// Wait for items (needed for heightmap + wired)
try {
itemsFuture.join();
} catch (Exception e) {
LOGGER.error("Error waiting for items to load", e);
}
// Phase 3: Heightmap and wired in parallel (both depend on items, not on each other)
CompletableFuture<Void> heightmapFuture = CompletableFuture.runAsync(() -> {
try {
this.loadHeightmap();
} catch (Exception e) {
LOGGER.error("Caught exception loading heightmap", e);
}
}, Emulator.getThreading().getService());
CompletableFuture<Void> wiredFuture = CompletableFuture.runAsync(() -> { CompletableFuture<Void> wiredFuture = CompletableFuture.runAsync(() -> {
try (Connection wiredConnection = Emulator.getDatabase().getDataSource().getConnection()) { try (Connection wiredConnection = Emulator.getDatabase().getDataSource().getConnection()) {
this.loadWiredData(wiredConnection); this.loadWiredData(wiredConnection);
@@ -553,9 +560,9 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
}, Emulator.getThreading().getService()); }, Emulator.getThreading().getService());
// Wait for all parallel operations to complete // Wait for all remaining operations
try { try {
CompletableFuture.allOf(rightsFuture, wordFilterFuture, botsFuture, petsFuture, wiredFuture).join(); CompletableFuture.allOf(rightsFuture, wordFilterFuture, botsFuture, petsFuture, heightmapFuture, wiredFuture).join();
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Error waiting for parallel room data loading", e); LOGGER.error("Error waiting for parallel room data loading", e);
} }
@@ -567,7 +574,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
this.roomCycleTask = Emulator.getThreading().getService() this.roomCycleTask = Emulator.getThreading().getService()
.scheduleAtFixedRate(this, 500, 500, TimeUnit.MILLISECONDS); .scheduleAtFixedRate(this, 500, 500, TimeUnit.MILLISECONDS);
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Caught exception during room load", e); LOGGER.error("Caught exception during room load", e);
} }
@@ -586,7 +593,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
item.setExtradata("1"); item.setExtradata("1");
this.updateItem(item); this.updateItem(item);
} }
// Set loaded flag with lock // Set loaded flag with lock
synchronized (this.loadLock) { synchronized (this.loadLock) {
this.loaded = true; this.loaded = true;
@@ -603,7 +610,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
this.layout = Emulator.getGameEnvironment().getRoomManager().loadCustomLayout(this); this.layout = Emulator.getGameEnvironment().getRoomManager().loadCustomLayout(this);
} else { } else {
this.layout = Emulator.getGameEnvironment().getRoomManager() this.layout = Emulator.getGameEnvironment().getRoomManager()
.loadLayout(this.layoutName, this); .loadLayout(this.layoutName, this);
} }
} }
} }
@@ -635,7 +642,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
this.unitManager.clearBots(); this.unitManager.clearBots();
try (PreparedStatement statement = connection.prepareStatement( try (PreparedStatement statement = connection.prepareStatement(
"SELECT users.username AS owner_name, bots.* FROM bots INNER JOIN users ON bots.user_id = users.id WHERE room_id = ?")) { "SELECT users.username AS owner_name, bots.* FROM bots INNER JOIN users ON bots.user_id = users.id WHERE room_id = ?")) {
statement.setInt(1, this.id); statement.setInt(1, this.id);
try (ResultSet set = statement.executeQuery()) { try (ResultSet set = statement.executeQuery()) {
while (set.next()) { while (set.next()) {
@@ -646,11 +653,11 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
b.setRoomUnit(new RoomUnit()); b.setRoomUnit(new RoomUnit());
b.getRoomUnit().setPathFinderRoom(this); b.getRoomUnit().setPathFinderRoom(this);
b.getRoomUnit() b.getRoomUnit()
.setLocation(this.layout.getTile((short) set.getInt("x"), (short) set.getInt("y"))); .setLocation(this.layout.getTile((short) set.getInt("x"), (short) set.getInt("y")));
if (b.getRoomUnit().getCurrentLocation() == null) { if (b.getRoomUnit().getCurrentLocation() == null) {
b.getRoomUnit().setLocation(this.getLayout().getDoorTile()); b.getRoomUnit().setLocation(this.getLayout().getDoorTile());
b.getRoomUnit() b.getRoomUnit()
.setRotation(RoomUserRotation.fromValue(this.getLayout().getDoorDirection())); .setRotation(RoomUserRotation.fromValue(this.getLayout().getDoorDirection()));
} else { } else {
b.getRoomUnit().setZ(set.getDouble("z")); b.getRoomUnit().setZ(set.getDouble("z"));
b.getRoomUnit().setPreviousLocationZ(set.getDouble("z")); b.getRoomUnit().setPreviousLocationZ(set.getDouble("z"));
@@ -674,7 +681,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
this.unitManager.clearPets(); this.unitManager.clearPets();
try (PreparedStatement statement = connection.prepareStatement( try (PreparedStatement statement = connection.prepareStatement(
"SELECT users.username as pet_owner_name, users_pets.* FROM users_pets INNER JOIN users ON users_pets.user_id = users.id WHERE room_id = ?")) { "SELECT users.username as pet_owner_name, users_pets.* FROM users_pets INNER JOIN users ON users_pets.user_id = users.id WHERE room_id = ?")) {
statement.setInt(1, this.id); statement.setInt(1, this.id);
try (ResultSet set = statement.executeQuery()) { try (ResultSet set = statement.executeQuery()) {
while (set.next()) { while (set.next()) {
@@ -684,11 +691,11 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
pet.setRoomUnit(new RoomUnit()); pet.setRoomUnit(new RoomUnit());
pet.getRoomUnit().setPathFinderRoom(this); pet.getRoomUnit().setPathFinderRoom(this);
pet.getRoomUnit() pet.getRoomUnit()
.setLocation(this.layout.getTile((short) set.getInt("x"), (short) set.getInt("y"))); .setLocation(this.layout.getTile((short) set.getInt("x"), (short) set.getInt("y")));
if (pet.getRoomUnit().getCurrentLocation() == null) { if (pet.getRoomUnit().getCurrentLocation() == null) {
pet.getRoomUnit().setLocation(this.getLayout().getDoorTile()); pet.getRoomUnit().setLocation(this.getLayout().getDoorTile());
pet.getRoomUnit() pet.getRoomUnit()
.setRotation(RoomUserRotation.fromValue(this.getLayout().getDoorDirection())); .setRotation(RoomUserRotation.fromValue(this.getLayout().getDoorDirection()));
} else { } else {
pet.getRoomUnit().setZ(set.getDouble("z")); pet.getRoomUnit().setZ(set.getDouble("z"));
pet.getRoomUnit().setRotation(RoomUserRotation.values()[set.getInt("rot")]); pet.getRoomUnit().setRotation(RoomUserRotation.values()[set.getInt("rot")]);
@@ -760,7 +767,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
THashSet<RoomTile> updatedTiles = new THashSet<>(); THashSet<RoomTile> updatedTiles = new THashSet<>();
Rectangle rectangle = RoomLayout.getRectangle(item.getX(), item.getY(), Rectangle rectangle = RoomLayout.getRectangle(item.getX(), item.getY(),
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), item.getRotation()); item.getBaseItem().getWidth(), item.getBaseItem().getLength(), item.getRotation());
for (short x = (short) rectangle.x; x < rectangle.x + rectangle.getWidth(); x++) { for (short x = (short) rectangle.x; x < rectangle.x + rectangle.getWidth(); x++) {
for (short y = (short) rectangle.y; y < rectangle.y + rectangle.getHeight(); y++) { for (short y = (short) rectangle.y; y < rectangle.y + rectangle.getHeight(); y++) {
@@ -784,7 +791,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
Habbo habbo = (picker != null && picker.getHabboInfo().getId() == item.getId() ? picker Habbo habbo = (picker != null && picker.getHabboInfo().getId() == item.getId() ? picker
: Emulator.getGameServer().getGameClientManager().getHabbo(item.getUserId())); : Emulator.getGameServer().getGameClientManager().getHabbo(item.getUserId()));
if (habbo != null) { if (habbo != null) {
habbo.getInventory().getItemsComponent().addItem(item); habbo.getInventory().getItemsComponent().addItem(item);
habbo.getClient().sendResponse(new AddHabboItemComposer(item)); habbo.getClient().sendResponse(new AddHabboItemComposer(item));
@@ -1022,7 +1029,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
message.appendInt(this.category); message.appendInt(this.category);
String[] tags = Arrays.stream(this.tags.split(";")).filter(t -> !t.isEmpty()) String[] tags = Arrays.stream(this.tags.split(";")).filter(t -> !t.isEmpty())
.toArray(String[]::new); .toArray(String[]::new);
message.appendInt(tags.length); message.appendInt(tags.length);
for (String s : tags) { for (String s : tags) {
message.appendString(s); message.appendString(s);
@@ -1086,8 +1093,8 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
public void save() { public void save() {
if (this.needsUpdate) { if (this.needsUpdate) {
try (Connection connection = Emulator.getDatabase().getDataSource() try (Connection connection = Emulator.getDatabase().getDataSource()
.getConnection(); PreparedStatement statement = connection.prepareStatement( .getConnection(); PreparedStatement statement = connection.prepareStatement(
"UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ? WHERE id = ?")) { "UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ? WHERE id = ?")) {
statement.setString(1, this.name); statement.setString(1, this.name);
statement.setString(2, this.description); statement.setString(2, this.description);
statement.setString(3, this.password); statement.setString(3, this.password);
@@ -1152,8 +1159,8 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
*/ */
public void updateDatabaseUserCount() { public void updateDatabaseUserCount() {
try (Connection connection = Emulator.getDatabase().getDataSource() try (Connection connection = Emulator.getDatabase().getDataSource()
.getConnection(); PreparedStatement statement = connection.prepareStatement( .getConnection(); PreparedStatement statement = connection.prepareStatement(
"UPDATE rooms SET users = ? WHERE id = ? LIMIT 1")) { "UPDATE rooms SET users = ? WHERE id = ? LIMIT 1")) {
statement.setInt(1, this.getUserCount()); statement.setInt(1, this.getUserCount());
statement.setInt(2, this.id); statement.setInt(2, this.id);
statement.executeUpdate(); statement.executeUpdate();
@@ -1467,7 +1474,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
if (extraData.length == 4) { if (extraData.length == 4) {
if (extraData[0].equalsIgnoreCase("1")) { if (extraData[0].equalsIgnoreCase("1")) {
return Color.getHSBColor(Integer.parseInt(extraData[1]), return Color.getHSBColor(Integer.parseInt(extraData[1]),
Integer.parseInt(extraData[2]), Integer.parseInt(extraData[3])); Integer.parseInt(extraData[2]), Integer.parseInt(extraData[3]));
} }
} }
} }
@@ -1574,7 +1581,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
public String[] filterAnything() { public String[] filterAnything() {
return new String[]{this.getOwnerName(), this.getGuildName(), this.getDescription(), return new String[]{this.getOwnerName(), this.getGuildName(), this.getDescription(),
this.getPromotionDesc()}; this.getPromotionDesc()};
} }
public long getCycleTimestamp() { public long getCycleTimestamp() {
@@ -1906,7 +1913,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
public void talk(final Habbo habbo, final RoomChatMessage roomChatMessage, RoomChatType chatType, public void talk(final Habbo habbo, final RoomChatMessage roomChatMessage, RoomChatType chatType,
boolean ignoreWired) { boolean ignoreWired) {
this.chatManager.talk(habbo, roomChatMessage, chatType, ignoreWired); this.chatManager.talk(habbo, roomChatMessage, chatType, ignoreWired);
} }
@@ -2051,7 +2058,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
private void loadRights(Connection connection) { private void loadRights(Connection connection) {
this.rights.clear(); this.rights.clear();
try (PreparedStatement statement = connection.prepareStatement( try (PreparedStatement statement = connection.prepareStatement(
"SELECT user_id FROM room_rights WHERE room_id = ?")) { "SELECT user_id FROM room_rights WHERE room_id = ?")) {
statement.setInt(1, this.id); statement.setInt(1, this.id);
try (ResultSet set = statement.executeQuery()) { try (ResultSet set = statement.executeQuery()) {
while (set.next()) { while (set.next()) {
@@ -2067,7 +2074,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
this.bannedHabbos.clear(); this.bannedHabbos.clear();
try (PreparedStatement statement = connection.prepareStatement( try (PreparedStatement statement = connection.prepareStatement(
"SELECT users.username, users.id, room_bans.* FROM room_bans INNER JOIN users ON room_bans.user_id = users.id WHERE ends > ? AND room_bans.room_id = ?")) { "SELECT users.username, users.id, room_bans.* FROM room_bans INNER JOIN users ON room_bans.user_id = users.id WHERE ends > ? AND room_bans.room_id = ?")) {
statement.setInt(1, Emulator.getIntUnixTimestamp()); statement.setInt(1, Emulator.getIntUnixTimestamp());
statement.setInt(2, this.id); statement.setInt(2, this.id);
try (ResultSet set = statement.executeQuery()) { try (ResultSet set = statement.executeQuery()) {
@@ -2089,7 +2096,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(this.guild); Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(this.guild);
if (Emulator.getGameEnvironment().getGuildManager().getOnlyAdmins(guild) if (Emulator.getGameEnvironment().getGuildManager().getOnlyAdmins(guild)
.get(habbo.getHabboInfo().getId()) != null) { .get(habbo.getHabboInfo().getId()) != null) {
return RoomRightLevels.GUILD_ADMIN; return RoomRightLevels.GUILD_ADMIN;
} }
@@ -2167,15 +2174,15 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
if (habbo.getRoomUnit().hasStatus(RoomUnitStatus.SIT) || !habbo.getRoomUnit() if (habbo.getRoomUnit().hasStatus(RoomUnitStatus.SIT) || !habbo.getRoomUnit()
.canForcePosture()) { .canForcePosture()) {
return; return;
} }
this.dance(habbo, DanceType.NONE); this.dance(habbo, DanceType.NONE);
habbo.getRoomUnit().cmdSit = true; habbo.getRoomUnit().cmdSit = true;
habbo.getRoomUnit().setBodyRotation( habbo.getRoomUnit().setBodyRotation(
RoomUserRotation.values()[habbo.getRoomUnit().getBodyRotation().getValue() RoomUserRotation.values()[habbo.getRoomUnit().getBodyRotation().getValue()
- habbo.getRoomUnit().getBodyRotation().getValue() % 2]); - habbo.getRoomUnit().getBodyRotation().getValue() % 2]);
habbo.getRoomUnit().setStatus(RoomUnitStatus.SIT, 0.5 + ""); habbo.getRoomUnit().setStatus(RoomUnitStatus.SIT, 0.5 + "");
this.sendComposer(new RoomUserStatusComposer(habbo.getRoomUnit()).compose()); this.sendComposer(new RoomUserStatusComposer(habbo.getRoomUnit()).compose());
WiredManager.triggerUserPerformsAction(this, habbo.getRoomUnit(), WiredUserActionType.SIT, -1); WiredManager.triggerUserPerformsAction(this, habbo.getRoomUnit(), WiredUserActionType.SIT, -1);
@@ -2189,11 +2196,11 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
HabboItem item = this.getTopItemAt(habbo.getRoomUnit().getX(), habbo.getRoomUnit().getY()); HabboItem item = this.getTopItemAt(habbo.getRoomUnit().getX(), habbo.getRoomUnit().getY());
if (item == null || !item.getBaseItem().allowSit() || !item.getBaseItem().allowLay()) { if (item == null || !item.getBaseItem().allowSit() || !item.getBaseItem().allowLay()) {
boolean wasSittingOrLaying = habbo.getRoomUnit().hasStatus(RoomUnitStatus.SIT) boolean wasSittingOrLaying = habbo.getRoomUnit().hasStatus(RoomUnitStatus.SIT)
|| habbo.getRoomUnit().hasStatus(RoomUnitStatus.LAY); || habbo.getRoomUnit().hasStatus(RoomUnitStatus.LAY);
habbo.getRoomUnit().cmdStand = true; habbo.getRoomUnit().cmdStand = true;
habbo.getRoomUnit().setBodyRotation( habbo.getRoomUnit().setBodyRotation(
RoomUserRotation.values()[habbo.getRoomUnit().getBodyRotation().getValue() RoomUserRotation.values()[habbo.getRoomUnit().getBodyRotation().getValue()
- habbo.getRoomUnit().getBodyRotation().getValue() % 2]); - habbo.getRoomUnit().getBodyRotation().getValue() % 2]);
habbo.getRoomUnit().removeStatus(RoomUnitStatus.SIT); habbo.getRoomUnit().removeStatus(RoomUnitStatus.SIT);
habbo.getRoomUnit().removeStatus(RoomUnitStatus.LAY); habbo.getRoomUnit().removeStatus(RoomUnitStatus.LAY);
this.sendComposer(new RoomUserStatusComposer(habbo.getRoomUnit()).compose()); this.sendComposer(new RoomUserStatusComposer(habbo.getRoomUnit()).compose());
@@ -2227,9 +2234,9 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
if (item.getBaseItem().getType() == FurnitureType.FLOOR) { if (item.getBaseItem().getType() == FurnitureType.FLOOR) {
this.sendComposer(new FloorItemUpdateComposer(item).compose()); this.sendComposer(new FloorItemUpdateComposer(item).compose());
this.updateTiles(this.getLayout() this.updateTiles(this.getLayout()
.getTilesAt(this.layout.getTile(item.getX(), item.getY()), .getTilesAt(this.layout.getTile(item.getX(), item.getY()),
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
item.getRotation())); item.getRotation()));
} else if (item.getBaseItem().getType() == FurnitureType.WALL) { } else if (item.getBaseItem().getType() == FurnitureType.WALL) {
this.sendComposer(new WallItemUpdateComposer(item).compose()); this.sendComposer(new WallItemUpdateComposer(item).compose());
} }
@@ -2251,8 +2258,8 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
this.updateTiles(this.getLayout() this.updateTiles(this.getLayout()
.getTilesAt(this.layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(), .getTilesAt(this.layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
item.getBaseItem().getLength(), item.getRotation())); item.getBaseItem().getLength(), item.getRotation()));
if (item instanceof InteractionMultiHeight) { if (item instanceof InteractionMultiHeight) {
((InteractionMultiHeight) item).updateUnitsOnItem(this); ((InteractionMultiHeight) item).updateUnitsOnItem(this);
@@ -2289,18 +2296,18 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
public void refreshGuild(Guild guild) { public void refreshGuild(Guild guild) {
if (guild.getRoomId() == this.id) { if (guild.getRoomId() == this.id) {
THashSet<GuildMember> members = Emulator.getGameEnvironment().getGuildManager() THashSet<GuildMember> members = Emulator.getGameEnvironment().getGuildManager()
.getGuildMembers(guild.getId()); .getGuildMembers(guild.getId());
for (Habbo habbo : this.getHabbos()) { for (Habbo habbo : this.getHabbos()) {
Optional<GuildMember> member = members.stream() Optional<GuildMember> member = members.stream()
.filter(m -> m.getUserId() == habbo.getHabboInfo().getId()).findAny(); .filter(m -> m.getUserId() == habbo.getHabboInfo().getId()).findAny();
if (!member.isPresent()) { if (!member.isPresent()) {
continue; continue;
} }
habbo.getClient() habbo.getClient()
.sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, member.get())); .sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, member.get()));
} }
} }
@@ -2335,7 +2342,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
if (habbo.getHabboInfo().getCurrentRoom() == this) { if (habbo.getHabboInfo().getCurrentRoom() == this) {
if (habbo.getHabboInfo().getId() != this.ownerId) { if (habbo.getHabboInfo().getId() != this.ownerId) {
if (!(habbo.hasPermission(Permission.ACC_ANYROOMOWNER) || habbo.hasPermission( if (!(habbo.hasPermission(Permission.ACC_ANYROOMOWNER) || habbo.hasPermission(
Permission.ACC_MOVEROTATE))) { Permission.ACC_MOVEROTATE))) {
this.refreshRightsForHabbo(habbo); this.refreshRightsForHabbo(habbo);
} }
} }
@@ -2421,18 +2428,18 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
} else { } else {
this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(), this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(),
this.roomSpecialTypes.getTriggers()).compose()); this.roomSpecialTypes.getTriggers()).compose());
this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(), this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(),
this.roomSpecialTypes.getEffects()).compose()); this.roomSpecialTypes.getEffects()).compose());
this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(), this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(),
this.roomSpecialTypes.getConditions()).compose()); this.roomSpecialTypes.getConditions()).compose());
this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(), this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(),
this.roomSpecialTypes.getExtras()).compose()); this.roomSpecialTypes.getExtras()).compose());
} }
} }
public FurnitureMovementError canPlaceFurnitureAt(HabboItem item, Habbo habbo, RoomTile tile, public FurnitureMovementError canPlaceFurnitureAt(HabboItem item, Habbo habbo, RoomTile tile,
int rotation) { int rotation) {
return this.itemManager.canPlaceFurnitureAt(item, habbo, tile, rotation); return this.itemManager.canPlaceFurnitureAt(item, habbo, tile, rotation);
} }
@@ -2441,17 +2448,17 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
public FurnitureMovementError furnitureFitsAt(RoomTile tile, HabboItem item, int rotation, public FurnitureMovementError furnitureFitsAt(RoomTile tile, HabboItem item, int rotation,
boolean checkForUnits) { boolean checkForUnits) {
return this.itemManager.furnitureFitsAt(tile, item, rotation, checkForUnits); return this.itemManager.furnitureFitsAt(tile, item, rotation, checkForUnits);
} }
public FurnitureMovementError furnitureFitsAtWithPhysics(RoomTile tile, HabboItem item, int rotation, public FurnitureMovementError furnitureFitsAtWithPhysics(RoomTile tile, HabboItem item, int rotation,
boolean checkForUnits, WiredMovementPhysics physics) { boolean checkForUnits, WiredMovementPhysics physics) {
return this.itemManager.furnitureFitsAtWithPhysics(tile, item, rotation, checkForUnits, physics); return this.itemManager.furnitureFitsAtWithPhysics(tile, item, rotation, checkForUnits, physics);
} }
public FurnitureMovementError placeFloorFurniAt(HabboItem item, RoomTile tile, int rotation, public FurnitureMovementError placeFloorFurniAt(HabboItem item, RoomTile tile, int rotation,
Habbo owner) { Habbo owner) {
return this.itemManager.placeFloorFurniAt(item, tile, rotation, owner); return this.itemManager.placeFloorFurniAt(item, tile, rotation, owner);
} }
@@ -2460,17 +2467,17 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
public FurnitureMovementError moveFurniTo(HabboItem item, RoomTile tile, int rotation, public FurnitureMovementError moveFurniTo(HabboItem item, RoomTile tile, int rotation,
Habbo actor) { Habbo actor) {
return this.itemManager.moveFurniTo(item, tile, rotation, actor); return this.itemManager.moveFurniTo(item, tile, rotation, actor);
} }
public FurnitureMovementError moveFurniTo(HabboItem item, RoomTile tile, int rotation, public FurnitureMovementError moveFurniTo(HabboItem item, RoomTile tile, int rotation,
Habbo actor, boolean sendUpdates) { Habbo actor, boolean sendUpdates) {
return this.itemManager.moveFurniTo(item, tile, rotation, actor, sendUpdates); return this.itemManager.moveFurniTo(item, tile, rotation, actor, sendUpdates);
} }
public FurnitureMovementError moveFurniTo(HabboItem item, RoomTile tile, int rotation, public FurnitureMovementError moveFurniTo(HabboItem item, RoomTile tile, int rotation,
Habbo actor, boolean sendUpdates, boolean checkForUnits) { Habbo actor, boolean sendUpdates, boolean checkForUnits) {
return this.itemManager.moveFurniTo(item, tile, rotation, actor, sendUpdates, checkForUnits); return this.itemManager.moveFurniTo(item, tile, rotation, actor, sendUpdates, checkForUnits);
} }
@@ -2487,12 +2494,12 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
} }
public FurnitureMovementError moveFurniToWithPhysics(HabboItem item, RoomTile tile, int rotation, public FurnitureMovementError moveFurniToWithPhysics(HabboItem item, RoomTile tile, int rotation,
Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) { Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) {
return this.itemManager.moveFurniToWithPhysics(item, tile, rotation, actor, sendUpdates, checkForUnits, physics); return this.itemManager.moveFurniToWithPhysics(item, tile, rotation, actor, sendUpdates, checkForUnits, physics);
} }
public FurnitureMovementError moveFurniToWithPhysics(HabboItem item, RoomTile tile, int rotation, double z, public FurnitureMovementError moveFurniToWithPhysics(HabboItem item, RoomTile tile, int rotation, double z,
Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) { Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) {
return this.itemManager.moveFurniToWithPhysics(item, tile, rotation, z, actor, sendUpdates, checkForUnits, physics); return this.itemManager.moveFurniToWithPhysics(item, tile, rotation, z, actor, sendUpdates, checkForUnits, physics);
} }
@@ -54,6 +54,25 @@ public class RoomLayout {
} }
} }
public RoomLayout(RoomManager.RoomLayoutData data, Room room) {
this.room = room;
try {
this.name = data.name;
this.doorX = (short) data.doorX;
this.doorY = (short) data.doorY;
this.doorDirection = data.doorDir;
this.heightmap = data.heightmap;
this.parse();
this.pathfinder = new PathfinderImpl(this.room, MAXIMUM_STEP_HEIGHT,
Emulator.getConfig().getBoolean("pathfinder.step.allow.falling", true),
Emulator.getConfig().getBoolean("pathfinder.retro-style.diagonals", false));
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
}
public static boolean squareInSquare(Rectangle outerSquare, Rectangle innerSquare) { public static boolean squareInSquare(Rectangle outerSquare, Rectangle innerSquare) {
if (outerSquare.x > innerSquare.x) { if (outerSquare.x > innerSquare.x) {
return false; return false;
@@ -72,6 +72,7 @@ public class RoomManager {
public static boolean SHOW_PUBLIC_IN_POPULAR_TAB = false; public static boolean SHOW_PUBLIC_IN_POPULAR_TAB = false;
private final THashMap<Integer, RoomCategory> roomCategories; private final THashMap<Integer, RoomCategory> roomCategories;
private final List<String> mapNames; private final List<String> mapNames;
private final ConcurrentHashMap<String, RoomLayoutData> layoutCache;
private final ConcurrentHashMap<Integer, Room> activeRooms; private final ConcurrentHashMap<Integer, Room> activeRooms;
private final ConcurrentHashMap<Integer, Set<Integer>> roomsByOwner; private final ConcurrentHashMap<Integer, Set<Integer>> roomsByOwner;
private final ArrayList<Class<? extends Game>> gameTypes; private final ArrayList<Class<? extends Game>> gameTypes;
@@ -80,6 +81,7 @@ public class RoomManager {
long millis = System.currentTimeMillis(); long millis = System.currentTimeMillis();
this.roomCategories = new THashMap<>(); this.roomCategories = new THashMap<>();
this.mapNames = new ArrayList<>(); this.mapNames = new ArrayList<>();
this.layoutCache = new ConcurrentHashMap<>();
this.activeRooms = new ConcurrentHashMap<>(); this.activeRooms = new ConcurrentHashMap<>();
this.roomsByOwner = new ConcurrentHashMap<>(); this.roomsByOwner = new ConcurrentHashMap<>();
this.loadRoomCategories(); this.loadRoomCategories();
@@ -114,9 +116,12 @@ public class RoomManager {
public void loadRoomModels() { public void loadRoomModels() {
this.mapNames.clear(); this.mapNames.clear();
this.layoutCache.clear();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM room_models")) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM room_models")) {
while (set.next()) { while (set.next()) {
this.mapNames.add(set.getString("name")); String name = set.getString("name");
this.mapNames.add(name);
this.layoutCache.put(name, new RoomLayoutData(set));
} }
} catch (SQLException e) { } catch (SQLException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
@@ -446,6 +451,12 @@ public class RoomManager {
} }
public RoomLayout loadLayout(String name, Room room) { public RoomLayout loadLayout(String name, Room room) {
RoomLayoutData cached = this.layoutCache.get(name);
if (cached != null) {
return new RoomLayout(cached, room);
}
// Fallback to DB if not in cache (should not happen for standard models)
RoomLayout layout = null; RoomLayout layout = null;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM room_models WHERE name = ? LIMIT 1")) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM room_models WHERE name = ? LIMIT 1")) {
statement.setString(1, name); statement.setString(1, name);
@@ -1623,4 +1634,24 @@ public class RoomManager {
this.duration = duration; this.duration = duration;
} }
} }
/**
* Cached layout data from room_models to avoid repeated DB queries.
* The raw data is shared; each Room gets its own RoomLayout instance.
*/
static class RoomLayoutData {
final String name;
final int doorX;
final int doorY;
final int doorDir;
final String heightmap;
RoomLayoutData(ResultSet set) throws SQLException {
this.name = set.getString("name");
this.doorX = set.getInt("door_x");
this.doorY = set.getInt("door_y");
this.doorDir = set.getInt("door_dir");
this.heightmap = set.getString("heightmap");
}
}
} }
@@ -517,17 +517,43 @@ public class RoomTileManager {
/** /**
* Loads the heightmap for the room. * Loads the heightmap for the room.
* Only updates tiles that have items on them (+ door tile) instead of all tiles,
* using getTilesAt() to correctly handle rotated multi-tile furniture.
*/ */
public void loadHeightmap() { public void loadHeightmap() {
RoomLayout layout = this.room.getLayout(); RoomLayout layout = this.room.getLayout();
if (layout != null) { if (layout != null) {
for (short x = 0; x < layout.getMapSizeX(); x++) { THashSet<HabboItem> floorItems = this.room.getFloorItems();
for (short y = 0; y < layout.getMapSizeY(); y++) {
RoomTile tile = layout.getTile(x, y); if (floorItems.isEmpty()) {
if (tile != null) { // No items - only update door tile
this.updateTile(tile); RoomTile doorTile = layout.getDoorTile();
} if (doorTile != null) {
this.updateTile(doorTile);
} }
return;
}
// Collect unique tiles occupied by items (handles rotation)
THashSet<RoomTile> tilesToUpdate = new THashSet<>();
for (HabboItem item : floorItems) {
RoomTile baseTile = layout.getTile(item.getX(), item.getY());
if (baseTile != null) {
tilesToUpdate.addAll(layout.getTilesAt(baseTile,
item.getBaseItem().getWidth(),
item.getBaseItem().getLength(),
item.getRotation()));
}
}
// Always include door tile
RoomTile doorTile = layout.getDoorTile();
if (doorTile != null) {
tilesToUpdate.add(doorTile);
}
for (RoomTile tile : tilesToUpdate) {
this.updateTile(tile);
} }
} else { } else {
LOGGER.error("Unknown Room Layout for Room (ID: {})", this.room.getId()); LOGGER.error("Unknown Room Layout for Room (ID: {})", this.room.getId());