diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index 40ff2aa0..c9bbfc09 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -285,6 +285,7 @@ public class PacketManager { this.registerHandler(Incoming.FurniEditorInteractionsEvent, FurniEditorInteractionsEvent.class); this.registerHandler(Incoming.FurniEditorUpdateEvent, FurniEditorUpdateEvent.class); this.registerHandler(Incoming.FurniEditorDeleteEvent, FurniEditorDeleteEvent.class); + this.registerHandler(Incoming.FurniEditorUpdateFurnidataEvent, FurniEditorUpdateFurnidataEvent.class); // Catalog Admin this.registerHandler(Incoming.CatalogAdminSavePageEvent, CatalogAdminSavePageEvent.class); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index bd8d1219..85259592 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -431,6 +431,8 @@ public class Incoming { public static final int FurniEditorInteractionsEvent = 10043; public static final int FurniEditorUpdateEvent = 10044; public static final int FurniEditorDeleteEvent = 10045; + public static final int FurniEditorUpdateFurnidataEvent = 10046; + public static final int FurniEditorRevertFurnidataEvent = 10048; // Catalog Admin public static final int CatalogAdminSavePageEvent = 10050; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdateFurnidataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdateFurnidataEvent.java new file mode 100644 index 00000000..5a9b2ecf --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdateFurnidataEvent.java @@ -0,0 +1,185 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.FurnidataEntry; +import com.eu.habbo.habbohotel.items.FurnidataLock; +import com.eu.habbo.habbohotel.items.FurnidataWriter; +import com.eu.habbo.habbohotel.items.FurnitureTextProvider; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.furniture.FurnitureDataReloadComposer; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Incoming handler 10046 — admin saves a furni name/description in the editor. + * + * Flow: permission check → rate-limit → resolve classname from item_id → + * under FurnidataLock: FurnidataWriter.write → FurnitureTextProvider.reindexFromSource → + * broadcast FurnitureDataReloadComposer (10047) → audit log → respond. + */ +public class FurniEditorUpdateFurnidataEvent extends MessageHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(FurniEditorUpdateFurnidataEvent.class); + + /** Rate-limit: min milliseconds between successive calls per admin user id. */ + private static final long RATE_LIMIT_MS = 1_000L; + + /** Per-admin last-call timestamp map. */ + private static final Map LAST_CALL = new ConcurrentHashMap<>(); + + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + + // 1. Permission check + if (!habbo.hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No permission")); + return; + } + + // 2. Rate-limit per admin + int adminId = habbo.getHabboInfo().getId(); + long now = System.currentTimeMillis(); + Long last = LAST_CALL.get(adminId); + if (last != null && (now - last) < RATE_LIMIT_MS) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Too many requests")); + return; + } + LAST_CALL.put(adminId, now); + + // 3. Read packet + int itemId = this.packet.readInt(); + JsonObject json; + try { + json = JsonParser.parseString(this.packet.readString()).getAsJsonObject(); + } catch (Exception e) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid JSON data")); + return; + } + + if (itemId <= 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid item ID")); + return; + } + + String name = json.has("name") ? json.get("name").getAsString() : null; + String description = json.has("description") ? json.get("description").getAsString() : null; + + if (name == null && description == null) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No name or description provided")); + return; + } + + // 4. Resolve classname from item_id + String classname = classnameForItem(itemId); + if (classname == null) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Item not found")); + return; + } + + // 5. Write + reindex + broadcast under the shared lock + FurnitureTextProvider provider = + Emulator.getGameEnvironment().getFurnitureTextProvider(); + + if (provider == null || provider.getSource() == null) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Furnidata source not configured")); + return; + } + + // Capture old values (before write) for the audit log + String oldName = provider.getName(classname); + // description is not indexed in the provider — treat as empty string for audit + String oldDesc = ""; + + // FurnidataWriter.write() calls FurnitureTextProvider.sanitize() internally; + // pass the raw values here and use them also for the audit log. + String safeName = (name != null) ? name : ""; + String safeDesc = (description != null) ? description : ""; + + boolean written; + List delta; + + FurnidataLock.LOCK.lock(); + try { + FurnidataWriter writer = new FurnidataWriter( + provider.getSource(), + provider.isSourceDirectory(), + provider.getMaxBytes(), + 3 /* backupKeep */ + ); + written = writer.write(classname, safeName, safeDesc); + if (!written) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Classname not found in furnidata")); + return; + } + + delta = provider.reindexFromSource(); + + if (!delta.isEmpty()) { + int deltaCap = Integer.parseInt( + Emulator.getConfig().getValue("items.furnidata.delta.cap", "500")); + FurnitureDataReloadComposer composer = (delta.size() > deltaCap) + ? new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_RELOAD_HINT, List.of()) + : new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_DELTA, delta); + broadcastToAll(composer); + } + } finally { + FurnidataLock.LOCK.unlock(); + } + + // 6. Audit log (outside lock — DB write, not latency-sensitive) + FurnidataAuditLog.record( + adminId, + classname, + "UPDATE_FURNIDATA", + oldName != null ? oldName : "", + safeName, + oldDesc, + safeDesc + ); + + // 7. Respond success + this.client.sendResponse(new FurniEditorResultComposer(true, "Furnidata updated", itemId)); + LOGGER.info("FurniEditorUpdateFurnidataEvent: admin {} updated furnidata for classname '{}' (item {})", + adminId, classname, itemId); + } + + /** + * Resolves the item_name (classname) from items_base for a given item id. + * Kept static so FurniEditorRevertFurnidataEvent can reuse it. + * + * @return the classname string, or {@code null} if not found or on error. + */ + public static String classnameForItem(int itemId) { + try (Connection c = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement st = c.prepareStatement("SELECT item_name FROM items_base WHERE id = ?")) { + st.setInt(1, itemId); + try (ResultSet rs = st.executeQuery()) { + if (rs.next()) return rs.getString("item_name"); + } + } catch (Exception e) { + LOGGER.warn("classnameForItem: failed to query items_base for id {}", itemId, e); + } + return null; + } + + private static void broadcastToAll(FurnitureDataReloadComposer composer) { + for (Habbo habbo : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().values()) { + if (habbo.getClient() != null) { + habbo.getClient().sendResponse(composer); + } + } + } +}