diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessageBubbles.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessageBubbles.java index d38133e4..fef00f33 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessageBubbles.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessageBubbles.java @@ -134,7 +134,18 @@ public class RoomChatMessageBubbles { } public static RoomChatMessageBubbles getBubble(int id) { - return BUBBLES.getOrDefault(id, NORMAL); + RoomChatMessageBubbles bubble = BUBBLES.get(id); + if (bubble != null) return bubble; + + // Custom chat bubbles (client-side only, e.g. ids 253+) are not registered + // above. Instead of falling back to NORMAL (which made them render as the + // default bubble), pass the id through so the server relays it as-is and + // the client renders its own .bubble- style. Capped to avoid abuse. + if (id > 0 && id <= 1000) { + return new RoomChatMessageBubbles(id, "CUSTOM_" + id, "", true, false); + } + + return NORMAL; } private static void registerBubble(RoomChatMessageBubbles bubble) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelManager.java index ba577dc4..729c6cc2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelManager.java @@ -12,9 +12,11 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.StringJoiner; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; @@ -330,26 +332,88 @@ public class WheelManager { return true; } - public void savePrize(int id, String type, String value, int amount, int pointsType, int weight, String label) { + /** + * Persists a single prize. An {@code id <= 0} inserts a brand-new prize and + * returns its generated id; a positive id updates the existing row (and + * re-enables it, so a previously soft-deleted prize can be brought back). + * {@code sortOrder} reflects the prize's position in the editor so the + * wheel layout matches what the admin sees. Returns the effective row id, + * or {@code 0} if the write failed. + */ + public int savePrize(int id, String type, String value, int amount, int pointsType, int weight, String label, int sortOrder) { String safeType = (type != null && VALID_PRIZE_TYPES.contains(type)) ? type : "nothing"; String safeValue = truncate(value, MAX_STRING_LEN); String safeLabel = truncate(label, MAX_STRING_LEN); int safeAmount = clamp(amount, 0, MAX_PRIZE_AMOUNT); int safeWeight = clamp(weight, 0, MAX_WEIGHT); + int safeSort = clamp(sortOrder, 0, MAX_PRIZES_PER_SAVE); + + if (id > 0) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "UPDATE wheel_prizes SET type = ?, value = ?, amount = ?, points_type = ?, weight = ?, label = ?, sort_order = ?, enabled = 1 WHERE id = ?")) { + statement.setString(1, safeType); + statement.setString(2, safeValue); + statement.setInt(3, safeAmount); + statement.setInt(4, pointsType); + statement.setInt(5, safeWeight); + statement.setString(6, safeLabel); + statement.setInt(7, safeSort); + statement.setInt(8, id); + statement.executeUpdate(); + return id; + } catch (SQLException e) { + LOGGER.error("Failed to save wheel prize {}", id, e); + return 0; + } + } try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "UPDATE wheel_prizes SET type = ?, value = ?, amount = ?, points_type = ?, weight = ?, label = ? WHERE id = ?")) { + "INSERT INTO wheel_prizes (type, value, amount, points_type, weight, label, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?, 1, ?)", + Statement.RETURN_GENERATED_KEYS)) { statement.setString(1, safeType); statement.setString(2, safeValue); statement.setInt(3, safeAmount); statement.setInt(4, pointsType); statement.setInt(5, safeWeight); statement.setString(6, safeLabel); - statement.setInt(7, id); + statement.setInt(7, safeSort); + statement.executeUpdate(); + + try (ResultSet keys = statement.getGeneratedKeys()) { + if (keys.next()) return keys.getInt(1); + } + } catch (SQLException e) { + LOGGER.error("Failed to insert wheel prize", e); + } + return 0; + } + + /** + * Soft-deletes every enabled prize whose id is not in {@code keptIds} by + * setting {@code enabled = 0}. This is intentionally non-destructive: rows + * stay in the table (so historical references and re-enabling remain + * possible) but {@link #loadPrizes()} only ever loads {@code enabled = 1}. + * An empty set disables all prizes. + */ + public void disablePrizesNotIn(Set keptIds) { + if (keptIds == null) return; + + StringBuilder sql = new StringBuilder("UPDATE wheel_prizes SET enabled = 0 WHERE enabled = 1"); + if (!keptIds.isEmpty()) { + StringJoiner ids = new StringJoiner(",", " AND id NOT IN (", ")"); + for (Integer keptId : keptIds) { + ids.add(Integer.toString(keptId)); + } + sql.append(ids); + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement(sql.toString())) { statement.executeUpdate(); } catch (SQLException e) { - LOGGER.error("Failed to save wheel prize {}", id, e); + LOGGER.error("Failed to disable removed wheel prizes", e); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/MachineIDEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/MachineIDEvent.java index 477fc3c6..64696bf7 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/MachineIDEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/MachineIDEvent.java @@ -1,5 +1,6 @@ package com.eu.habbo.messages.incoming.handshake; +import com.eu.habbo.Emulator; import com.eu.habbo.messages.NoAuthMessage; import com.eu.habbo.messages.incoming.MessageHandler; import org.slf4j.Logger; @@ -24,6 +25,15 @@ public class MachineIDEvent extends MessageHandler { this.client.setMachineId(storedMachineId); + // Persist the machine fingerprint onto the user so machine/super bans can + // target it (createOfflineUserBan copies users.machine_id). The Nitro client + // sends this UniqueID packet right after the SSO ticket, so the Habbo is + // normally already loaded by the time we get here. + if (!storedMachineId.isEmpty() && this.client.getHabbo() != null && this.client.getHabbo().getHabboInfo() != null) { + this.client.getHabbo().getHabboInfo().setMachineID(storedMachineId); + Emulator.getThreading().run(this.client.getHabbo()); + } + LOGGER.debug("Setting client MachineId to {}", storedMachineId); } } \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java index 7c7dcf3c..cdebe27d 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java @@ -161,6 +161,12 @@ public class SecureLoginEvent extends MessageHandler { throw new NullPointerException(habbo.getHabboInfo().getUsername() + " has a NON EXISTING RANK!"); } + // If the machine fingerprint already arrived (UniqueID before login), + // persist it so machine/super bans can target this user. + if (this.client.getMachineId() != null && !this.client.getMachineId().isEmpty()) { + this.client.getHabbo().getHabboInfo().setMachineID(this.client.getMachineId()); + } + Emulator.getThreading().run(habbo); Emulator.getGameEnvironment().getHabboManager().addHabbo(habbo); } catch (Exception e) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminSavePrizesEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminSavePrizesEvent.java index 3dbf9235..41093c72 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminSavePrizesEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminSavePrizesEvent.java @@ -6,6 +6,9 @@ import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.wheel.WheelAdminPrizesComposer; import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer; +import java.util.HashSet; +import java.util.Set; + public class WheelAdminSavePrizesEvent extends MessageHandler { public static final String PERMISSION_KEY = "acc_wheeladmin"; @@ -25,6 +28,12 @@ public class WheelAdminSavePrizesEvent extends MessageHandler { int count = this.packet.readInt(); if (count <= 0 || count > WheelManager.MAX_PRIZES_PER_SAVE) return; + // The client sends the full authoritative list of prizes in display + // order. id <= 0 means "insert a new prize"; any existing prize whose + // id is absent from this list was removed in the editor and gets + // soft-disabled below. + Set keptIds = new HashSet<>(); + for (int i = 0; i < count; i++) { int id = this.packet.readInt(); String type = this.packet.readString(); @@ -33,9 +42,11 @@ public class WheelAdminSavePrizesEvent extends MessageHandler { int pointsType = this.packet.readInt(); int weight = this.packet.readInt(); String label = this.packet.readString(); - wheel.savePrize(id, type, value, amount, pointsType, weight, label); + int savedId = wheel.savePrize(id, type, value, amount, pointsType, weight, label, i); + if (savedId > 0) keptIds.add(savedId); } + wheel.disablePrizesNotIn(keptIds); wheel.reload(); this.client.sendResponse(new WheelAdminPrizesComposer(wheel.getPrizes()));