diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java
index b760e4de..dc29be02 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java
@@ -89,6 +89,7 @@ public class HabboStats implements Runnable {
public long lastTradeTimestamp = Emulator.getIntUnixTimestamp();
public long lastGiftTimestamp = Emulator.getIntUnixTimestamp();
public long lastPurchaseTimestamp = Emulator.getIntUnixTimestamp();
+ public long lastFloorplanSaveTimestamp = 0;
public int uiFlags;
public boolean hasGottenDefaultSavedSearches;
private HabboInfo habboInfo;
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/floorplaneditor/FloorPlanEditorSaveEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/floorplaneditor/FloorPlanEditorSaveEvent.java
index 48ec258a..b40f9300 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/floorplaneditor/FloorPlanEditorSaveEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/floorplaneditor/FloorPlanEditorSaveEvent.java
@@ -11,16 +11,24 @@ import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer;
import com.eu.habbo.messages.outgoing.rooms.ForwardToRoomComposer;
import gnu.trove.set.hash.THashSet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.StringJoiner;
+import java.util.regex.Pattern;
public class FloorPlanEditorSaveEvent extends MessageHandler {
+ private static final Logger LOGGER = LoggerFactory.getLogger(FloorPlanEditorSaveEvent.class);
+
public static int MAXIMUM_FLOORPLAN_WIDTH_LENGTH = 64;
public static int MAXIMUM_FLOORPLAN_SIZE = 64 * 64;
+ private static final int SAVE_COOLDOWN_SECONDS = 3;
+ private static final Pattern ALLOWED_MAP_CHARS = Pattern.compile("[a-qA-Q0-9xX\r]+");
+
@Override
public int getRatelimit() {
return 500;
@@ -38,154 +46,184 @@ public class FloorPlanEditorSaveEvent extends MessageHandler {
if (room == null)
return;
- if (room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) {
- StringJoiner errors = new StringJoiner("
");
- String map = this.packet.readString();
- map = map.replace("X", "x");
+ if (!(room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER))) {
+ return;
+ }
- String[] mapRows = map.split("\r");
+ long now = Emulator.getIntUnixTimestamp();
+ if (now - this.client.getHabbo().getHabboStats().lastFloorplanSaveTimestamp < SAVE_COOLDOWN_SECONDS) {
+ this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, "Please wait a few seconds before saving again."));
+ return;
+ }
- int firstRowSize = mapRows[0].length();
+ StringJoiner errors = new StringJoiner("
");
+ String map = this.packet.readString();
- if (Emulator.getConfig().getBoolean("hotel.room.floorplan.check.enabled")) {
- if (!map.matches("[a-zA-Z0-9\r]+")) errors.add("${notification.floorplan_editor.error.title}");
+ if (map == null || map.length() > MAXIMUM_FLOORPLAN_SIZE) {
+ LOGGER.warn("Floorplan save rejected (oversize): user={} room={} mapLen={}",
+ this.client.getHabbo().getHabboInfo().getId(), room.getId(), map == null ? 0 : map.length());
+ return;
+ }
- Arrays.stream(mapRows)
- .filter(line -> line.length() != firstRowSize)
- .findAny()
- .ifPresent(s -> errors.add("(General): Line " + (Arrays.asList(mapRows).indexOf(s) + 1) + " is of different length than line 1"));
+ if (!ALLOWED_MAP_CHARS.matcher(map).matches()) {
+ LOGGER.warn("Floorplan save rejected (illegal chars): user={} room={}",
+ this.client.getHabbo().getHabboInfo().getId(), room.getId());
+ return;
+ }
- if (map.isEmpty() || map.replace("x", "").replace("\r", "").isEmpty()) {
- errors.add("${notification.floorplan_editor.error.message.effective_height_is_0}");
- }
+ map = map.replace("X", "x");
- if (map.length() > MAXIMUM_FLOORPLAN_SIZE) {
- errors.add("${notification.floorplan_editor.error.message.too_large_area}");
- }
+ String[] mapRows = map.split("\r");
- if (mapRows.length > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) errors.add("${notification.floorplan_editor.error.message.too_large_height}");
- else if (Arrays.stream(mapRows).anyMatch(l -> l.length() > MAXIMUM_FLOORPLAN_WIDTH_LENGTH || l.isEmpty())) errors.add("${notification.floorplan_editor.error.message.too_large_width}");
+ if (mapRows.length == 0 || mapRows.length > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) {
+ return;
+ }
- if (errors.length() > 0) {
- this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString()));
- return;
- }
- }
+ int firstRowSize = mapRows[0].length();
- int doorX = this.packet.readInt();
- int doorY = this.packet.readInt();
+ if (firstRowSize == 0 || firstRowSize > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) {
+ return;
+ }
- if (doorX < 0 || doorX > firstRowSize || doorY < 0 || doorY >= mapRows.length) {
- errors.add("${notification.floorplan_editor.error.message.entry_tile_outside_map}");
- }
-
- if (doorY < mapRows.length && doorX < mapRows[doorY].length() && mapRows[doorY].charAt(doorX) == 'x') {
- errors.add("${notification.floorplan_editor.error.message.entry_not_on_tile}");
- }
-
- int doorRotation = this.packet.readInt();
- if (doorRotation < 0 || doorRotation > 7) {
- errors.add("${notification.floorplan_editor.error.message.invalid_entry_tile_direction}");
- }
-
- int wallSize = this.packet.readInt();
- if (wallSize < -2 || wallSize > 1) {
- errors.add("${notification.floorplan_editor.error.message.invalid_wall_thickness}");
- }
- int floorSize = this.packet.readInt();
- if (floorSize < -2 || floorSize > 1) {
- errors.add("${notification.floorplan_editor.error.message.invalid_floor_thickness}");
- }
-
- int wallHeight = -1;
- if (this.packet.bytesAvailable() >= 4)
- wallHeight = this.packet.readInt();
-
- if (wallHeight < -1 || wallHeight > 15) {
- errors.add("${notification.floorplan_editor.error.message.invalid_walls_fixed_height}");
- }
-
- THashSet locked_tileList = room.getLockedTiles();
- THashSet new_tileList = new THashSet<>();
- blockingRoomItemScan:
- for (int y = 0; y < mapRows.length; y++) {
- for (int x = 0; x < firstRowSize; x++) {
-
- RoomTile tile = room.getLayout().getTile((short) x, (short) y);
- new_tileList.add(tile);
- String square = String.valueOf(mapRows[y].charAt(x));
- short height;
-
- if (square.equalsIgnoreCase("x") && room.getTopItemAt(x, y) != null) {
- errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
- break blockingRoomItemScan;
- } else {
- if (square.isEmpty()) {
- height = 0;
- } else if (Emulator.isNumeric(square)) {
- height = Short.parseShort(square);
- } else {
- height = (short) (10 + "ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(square.toUpperCase()));
- }
- }
-
- if (tile != null && tile.state != RoomTileState.INVALID && height != tile.z && room.getTopItemAt(x, y) != null) {
- errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
- break blockingRoomItemScan;
- }
- }
- }
-
- locked_tileList.removeAll(new_tileList);
- if (!locked_tileList.isEmpty()) {
- errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
- }
-
-
-
- if (errors.length() > 0) {
- this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString()));
+ for (String row : mapRows) {
+ if (row.length() != firstRowSize) {
return;
}
+ }
- RoomLayout layout = room.getLayout();
+ if (Emulator.getConfig().getBoolean("hotel.room.floorplan.check.enabled")) {
+ if (map.replace("x", "").replace("\r", "").isEmpty()) {
+ errors.add("${notification.floorplan_editor.error.message.effective_height_is_0}");
+ }
+ }
- if (layout instanceof CustomRoomLayout) {
- layout.setDoorX((short) doorX);
- layout.setDoorY((short) doorY);
- layout.setDoorDirection(doorRotation);
- layout.setHeightmap(map);
- layout.parse();
+ int doorX = this.packet.readInt();
+ int doorY = this.packet.readInt();
- if (layout.getDoorTile() == null) {
- this.client.getHabbo().alert("Error");
- ((CustomRoomLayout) layout).needsUpdate(false);
- Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
+ if (doorX < 0 || doorX >= firstRowSize || doorY < 0 || doorY >= mapRows.length) {
+ errors.add("${notification.floorplan_editor.error.message.entry_tile_outside_map}");
+ } else if (mapRows[doorY].charAt(doorX) == 'x') {
+ errors.add("${notification.floorplan_editor.error.message.entry_not_on_tile}");
+ }
+
+ int doorRotation = this.packet.readInt();
+ if (doorRotation < 0 || doorRotation > 7) {
+ errors.add("${notification.floorplan_editor.error.message.invalid_entry_tile_direction}");
+ }
+
+ int wallSize = this.packet.readInt();
+ if (wallSize < -2 || wallSize > 1) {
+ errors.add("${notification.floorplan_editor.error.message.invalid_wall_thickness}");
+ }
+ int floorSize = this.packet.readInt();
+ if (floorSize < -2 || floorSize > 1) {
+ errors.add("${notification.floorplan_editor.error.message.invalid_floor_thickness}");
+ }
+
+ int wallHeight = -1;
+ if (this.packet.bytesAvailable() >= 4)
+ wallHeight = this.packet.readInt();
+
+ if (wallHeight < -1 || wallHeight > 15) {
+ errors.add("${notification.floorplan_editor.error.message.invalid_walls_fixed_height}");
+ }
+
+ if (errors.length() > 0) {
+ this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString()));
+ return;
+ }
+
+ THashSet locked_tileList = room.getLockedTiles();
+ THashSet new_tileList = new THashSet<>();
+ blockingRoomItemScan:
+ for (int y = 0; y < mapRows.length; y++) {
+ for (int x = 0; x < firstRowSize; x++) {
+
+ RoomTile tile = room.getLayout().getTile((short) x, (short) y);
+ new_tileList.add(tile);
+ String square = String.valueOf(mapRows[y].charAt(x));
+ short height;
+
+ if (square.equalsIgnoreCase("x") && room.getTopItemAt(x, y) != null) {
+ errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
+ break blockingRoomItemScan;
+ }
+
+ try {
+ if (square.isEmpty()) {
+ height = 0;
+ } else if (Emulator.isNumeric(square)) {
+ height = Short.parseShort(square);
+ } else {
+ int idx = "abcdefghijklmnopq".indexOf(square.toLowerCase());
+ if (idx < 0) {
+ return;
+ }
+ height = (short) (10 + idx);
+ }
+ } catch (NumberFormatException e) {
return;
}
- ((CustomRoomLayout) layout).needsUpdate(true);
- Emulator.getThreading().run((CustomRoomLayout) layout);
- } else {
- layout = Emulator.getGameEnvironment().getRoomManager().insertCustomLayout(room, map, doorX, doorY, doorRotation);
- }
- if (layout != null) {
- room.setHasCustomLayout(true);
- room.setNeedsUpdate(true);
- room.setLayout(layout);
- room.setWallSize(wallSize);
- room.setFloorSize(floorSize);
- room.setWallHeight(wallHeight);
- room.save();
- Collection habbos = new ArrayList<>(room.getUserCount());
- habbos.addAll(room.getHabbos());
- Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
- room = Emulator.getGameEnvironment().getRoomManager().loadRoom(room.getId());
- ServerMessage message = new ForwardToRoomComposer(room.getId()).compose();
- for (Habbo habbo : habbos) {
- habbo.getClient().sendResponse(message);
+ if (tile != null && tile.state != RoomTileState.INVALID && height != tile.z && room.getTopItemAt(x, y) != null) {
+ errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
+ break blockingRoomItemScan;
}
}
}
+
+ locked_tileList.removeAll(new_tileList);
+ if (!locked_tileList.isEmpty()) {
+ errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
+ }
+
+ if (errors.length() > 0) {
+ this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString()));
+ return;
+ }
+
+ RoomLayout layout = room.getLayout();
+
+ if (layout instanceof CustomRoomLayout) {
+ layout.setDoorX((short) doorX);
+ layout.setDoorY((short) doorY);
+ layout.setDoorDirection(doorRotation);
+ layout.setHeightmap(map);
+ layout.parse();
+
+ if (layout.getDoorTile() == null) {
+ this.client.getHabbo().alert("Error");
+ ((CustomRoomLayout) layout).needsUpdate(false);
+ Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
+ return;
+ }
+ ((CustomRoomLayout) layout).needsUpdate(true);
+ Emulator.getThreading().run((CustomRoomLayout) layout);
+ } else {
+ layout = Emulator.getGameEnvironment().getRoomManager().insertCustomLayout(room, map, doorX, doorY, doorRotation);
+ }
+
+ if (layout != null) {
+ room.setHasCustomLayout(true);
+ room.setNeedsUpdate(true);
+ room.setLayout(layout);
+ room.setWallSize(wallSize);
+ room.setFloorSize(floorSize);
+ room.setWallHeight(wallHeight);
+ room.save();
+
+ this.client.getHabbo().getHabboStats().lastFloorplanSaveTimestamp = now;
+ LOGGER.info("Floorplan saved: user={} room={} mapLen={} rows={} cols={}",
+ this.client.getHabbo().getHabboInfo().getId(), room.getId(), map.length(), mapRows.length, firstRowSize);
+
+ Collection habbos = new ArrayList<>(room.getUserCount());
+ habbos.addAll(room.getHabbos());
+ Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
+ room = Emulator.getGameEnvironment().getRoomManager().loadRoom(room.getId());
+ ServerMessage message = new ForwardToRoomComposer(room.getId()).compose();
+ for (Habbo habbo : habbos) {
+ habbo.getClient().sendResponse(message);
+ }
+ }
}
}