From 9d98fbf9eef1f2844bcab4813964e8fb18d71a5d Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 31 May 2026 10:49:10 +0200 Subject: [PATCH] feat(wheel): support adding & removing fortune-wheel prizes from the editor The prize editor could only update existing rows; savePrize was UPDATE-only, so the admin panel had no way to add a new slice or remove an old one. - WheelManager.savePrize now takes a sortOrder and inserts when id <= 0 (returning the generated id) or updates + re-enables when id > 0, so a previously removed prize can be brought back. sort_order is persisted to match the editor's display order. - New WheelManager.disablePrizesNotIn(keptIds) soft-deletes (enabled = 0) any prize absent from the saved authoritative list. Non-destructive: rows stay in the table and loadPrizes already filters enabled = 1. - WheelAdminSavePrizesEvent collects the saved ids and disables the rest before reloading. No schema change (wheel_prizes already has enabled + sort_order) and no packet change (id = 0 / omission express insert / delete on the existing wire). Pairs with the Nitro-V3 client editor add/remove buttons. --- .../habbo/habbohotel/wheel/WheelManager.java | 72 +++++++++++++++++-- .../wheel/WheelAdminSavePrizesEvent.java | 13 +++- 2 files changed, 80 insertions(+), 5 deletions(-) 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/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()));