diff --git a/Database Updates/furni_editor.sql b/Database Updates/furni_editor.sql new file mode 100644 index 00000000..b7e391c7 --- /dev/null +++ b/Database Updates/furni_editor.sql @@ -0,0 +1,4 @@ +-- FurniEditor: emulator config keys for FurniDataManager +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES + ('furni.editor.renderer.config.path', 'E:/www/habbo-next/public/nitro3/public/renderer-config.json'), + ('furni.editor.asset.base.path', 'E:/www/habbo-next/public/nitro-assets/'); 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 885ec904..0c6f8ebd 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -11,6 +11,7 @@ import com.eu.habbo.messages.incoming.ambassadors.AmbassadorVisitCommandEvent; import com.eu.habbo.messages.incoming.camera.*; import com.eu.habbo.messages.incoming.catalog.*; import com.eu.habbo.messages.incoming.catalog.catalogadmin.*; +import com.eu.habbo.messages.incoming.furnieditor.*; import com.eu.habbo.messages.incoming.catalog.marketplace.*; import com.eu.habbo.messages.incoming.catalog.recycler.OpenRecycleBoxEvent; import com.eu.habbo.messages.incoming.catalog.recycler.RecycleEvent; @@ -260,6 +261,14 @@ public class PacketManager { this.registerHandler(Incoming.CatalogRequestClubDiscountEvent, CatalogRequestClubDiscountEvent.class); this.registerHandler(Incoming.CatalogBuyClubDiscountEvent, CatalogBuyClubDiscountEvent.class); + // Furni Editor + this.registerHandler(Incoming.FurniEditorSearchEvent, FurniEditorSearchEvent.class); + this.registerHandler(Incoming.FurniEditorDetailEvent, FurniEditorDetailEvent.class); + this.registerHandler(Incoming.FurniEditorBySpriteEvent, FurniEditorBySpriteEvent.class); + this.registerHandler(Incoming.FurniEditorInteractionsEvent, FurniEditorInteractionsEvent.class); + this.registerHandler(Incoming.FurniEditorUpdateEvent, FurniEditorUpdateEvent.class); + this.registerHandler(Incoming.FurniEditorDeleteEvent, FurniEditorDeleteEvent.class); + // Catalog Admin this.registerHandler(Incoming.CatalogAdminSavePageEvent, CatalogAdminSavePageEvent.class); this.registerHandler(Incoming.CatalogAdminCreatePageEvent, CatalogAdminCreatePageEvent.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 20c7e469..9b99e7ab 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 @@ -412,6 +412,14 @@ public class Incoming { public static final int RequestInventoryPetDelete = 10030; public static final int RequestInventoryBadgeDelete = 10031; + // Furni Editor + public static final int FurniEditorSearchEvent = 10040; + public static final int FurniEditorDetailEvent = 10041; + public static final int FurniEditorBySpriteEvent = 10042; + public static final int FurniEditorInteractionsEvent = 10043; + public static final int FurniEditorUpdateEvent = 10044; + public static final int FurniEditorDeleteEvent = 10045; + // Catalog Admin public static final int CatalogAdminSavePageEvent = 10050; public static final int CatalogAdminCreatePageEvent = 10051; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniDataManager.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniDataManager.java new file mode 100644 index 00000000..8035bb9b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniDataManager.java @@ -0,0 +1,119 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Manages reading and writing of FurnitureData.json entries. + * Resolves the file path from emulator config keys. + */ +public class FurniDataManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(FurniDataManager.class); + + /** + * Get the JSON string for a specific item from FurnitureData.json. + * Returns "{}" if not found or on error. + */ + public static String getItemJson(int itemId) { + try { + Path furniDataPath = resolveFurniDataPath(); + if (furniDataPath == null || !Files.exists(furniDataPath)) { + return "{}"; + } + + String content = Files.readString(furniDataPath, StandardCharsets.UTF_8); + JsonObject root = JsonParser.parseString(content).getAsJsonObject(); + + // Search in both "roomitemtypes" and "wallitemtypes" + for (String section : new String[]{"roomitemtypes", "wallitemtypes"}) { + if (!root.has(section)) continue; + JsonObject sectionObj = root.getAsJsonObject(section); + if (!sectionObj.has("furnitype")) continue; + JsonArray types = sectionObj.getAsJsonArray("furnitype"); + + for (JsonElement el : types) { + JsonObject obj = el.getAsJsonObject(); + if (obj.has("id") && obj.get("id").getAsInt() == itemId) { + return obj.toString(); + } + } + } + } catch (Exception e) { + LOGGER.warn("Failed to read FurnitureData.json for item " + itemId, e); + } + + return "{}"; + } + + /** + * Resolve the path to FurnitureData.json from emulator config. + */ + private static Path resolveFurniDataPath() { + try { + String configPath = Emulator.getConfig().getValue("furni.editor.renderer.config.path", ""); + + if (configPath.isEmpty()) { + // Fallback: try common locations + String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", ""); + if (!basePath.isEmpty()) { + Path candidate = Paths.get(basePath, "FurnitureData.json"); + if (Files.exists(candidate)) return candidate; + } + return null; + } + + // Read the renderer config to find the furnidata URL/path + Path rendererConfig = Paths.get(configPath); + if (!Files.exists(rendererConfig)) return null; + + String rendererContent = Files.readString(rendererConfig, StandardCharsets.UTF_8); + JsonObject rendererObj = JsonParser.parseString(rendererContent).getAsJsonObject(); + + if (rendererObj.has("furnidata.url")) { + String furniUrl = rendererObj.get("furnidata.url").getAsString(); + + // Skip unresolved placeholders like ${gamedata.url} + if (furniUrl.contains("${")) { + String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", ""); + if (!basePath.isEmpty()) { + Path candidate = Paths.get(basePath, "FurnitureData.json"); + if (Files.exists(candidate)) return candidate; + } + return null; + } + + // Strip query string (?v=1 etc.) + String cleanUrl = furniUrl.contains("?") ? furniUrl.substring(0, furniUrl.indexOf('?')) : furniUrl; + + // If it's a local file path (not http), use it directly + if (!cleanUrl.startsWith("http")) { + return Paths.get(cleanUrl); + } + + // For http URLs, try to derive local path from base path + String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", ""); + if (!basePath.isEmpty()) { + // Extract filename from URL (without query string) + String filename = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1); + return Paths.get(basePath, filename); + } + } + } catch (Exception e) { + LOGGER.warn("Failed to resolve FurnitureData.json path", e); + } + + return null; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorBySpriteEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorBySpriteEvent.java new file mode 100644 index 00000000..e7f45362 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorBySpriteEvent.java @@ -0,0 +1,49 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +public class FurniEditorBySpriteEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No permission")); + return; + } + + int spriteId = this.packet.readInt(); + + if (spriteId <= 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid sprite ID")); + return; + } + + // Look up the item ID by sprite_id + int itemId = -1; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement stmt = connection.prepareStatement("SELECT id FROM items_base WHERE sprite_id = ? LIMIT 1")) { + stmt.setInt(1, spriteId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + itemId = rs.getInt("id"); + } + } + } + + if (itemId <= 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No item found with sprite_id: " + spriteId)); + return; + } + + // Delegate to the detail response builder + FurniEditorDetailEvent.sendDetailResponse(this.client, itemId); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDeleteEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDeleteEvent.java new file mode 100644 index 00000000..b31194dd --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDeleteEvent.java @@ -0,0 +1,87 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +public class FurniEditorDeleteEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No permission")); + return; + } + + int id = this.packet.readInt(); + + if (id <= 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid item ID")); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + // Check if item exists + try (PreparedStatement stmt = connection.prepareStatement("SELECT id FROM items_base WHERE id = ?")) { + stmt.setInt(1, id); + try (ResultSet rs = stmt.executeQuery()) { + if (!rs.next()) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Item not found: " + id)); + return; + } + } + } + + // Check usage count - items placed in rooms + int usageCount = 0; + try (PreparedStatement stmt = connection.prepareStatement("SELECT COUNT(*) FROM items WHERE item_id = ?")) { + stmt.setInt(1, id); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + usageCount = rs.getInt(1); + } + } + } + + if (usageCount > 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, + "Cannot delete: " + usageCount + " instances exist in the game")); + return; + } + + // Check catalog_items references + int catalogCount = 0; + try (PreparedStatement stmt = connection.prepareStatement( + "SELECT COUNT(*) FROM catalog_items WHERE item_ids LIKE ?")) { + stmt.setString(1, "%" + id + "%"); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + catalogCount = rs.getInt(1); + } + } + } + + if (catalogCount > 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, + "Cannot delete: item is referenced by " + catalogCount + " catalog entries")); + return; + } + + // Safe to delete + try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM items_base WHERE id = ?")) { + stmt.setInt(1, id); + stmt.executeUpdate(); + } + } + + // Reload emulator item definitions + Emulator.getGameEnvironment().getItemManager().loadItems(); + + this.client.sendResponse(new FurniEditorResultComposer(true, "Item deleted")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDetailEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDetailEvent.java new file mode 100644 index 00000000..b904fb55 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDetailEvent.java @@ -0,0 +1,96 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorDetailComposer; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class FurniEditorDetailEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No permission")); + return; + } + + int id = this.packet.readInt(); + + if (id <= 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid item ID")); + return; + } + + sendDetailResponse(this.client, id); + } + + /** + * Shared method to build and send a detail response for a given item ID. + * Used by both FurniEditorDetailEvent and FurniEditorBySpriteEvent. + */ + public static void sendDetailResponse(com.eu.habbo.habbohotel.gameclients.GameClient client, int itemId) throws Exception { + Map item = null; + int usageCount = 0; + List> catalogItems = new ArrayList<>(); + String furniDataJson = "{}"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + // Load full item data + try (PreparedStatement stmt = connection.prepareStatement("SELECT * FROM items_base WHERE id = ?")) { + stmt.setInt(1, itemId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + item = FurniEditorHelper.readFullItem(rs); + } + } + } + + if (item == null) { + client.sendResponse(new FurniEditorResultComposer(false, "Item not found: " + itemId)); + return; + } + + // Count placed instances + try (PreparedStatement stmt = connection.prepareStatement("SELECT COUNT(*) FROM items WHERE item_id = ?")) { + stmt.setInt(1, itemId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + usageCount = rs.getInt(1); + } + } + } + + // Load catalog references (join catalog_items with catalog_pages) + try (PreparedStatement stmt = connection.prepareStatement( + "SELECT ci.id AS ci_id, ci.catalog_name, ci.cost_credits, ci.cost_points, ci.points_type, " + + "ci.page_id AS ci_page_id, COALESCE(cp.caption, '') AS page_caption " + + "FROM catalog_items ci " + + "LEFT JOIN catalog_pages cp ON ci.page_id = cp.id " + + "WHERE ci.item_ids LIKE ?")) { + stmt.setString(1, "%" + itemId + "%"); + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + catalogItems.add(FurniEditorHelper.readCatalogRef(rs)); + } + } + } + } + + // Try to read furnidata.json entry + try { + furniDataJson = FurniDataManager.getItemJson(itemId); + } catch (Exception e) { + furniDataJson = "{}"; + } + + client.sendResponse(new FurniEditorDetailComposer(item, usageCount, catalogItems, furniDataJson)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorHelper.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorHelper.java new file mode 100644 index 00000000..0de0e1b6 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorHelper.java @@ -0,0 +1,123 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +/** + * Shared utility for building item data maps from ResultSet rows. + * Used by FurniEditorDetailEvent, FurniEditorBySpriteEvent, and + * FurniEditorSearchEvent to ensure consistent field reading. + */ +public class FurniEditorHelper { + + /** + * Read the 14 base fields from items_base into a Map. + */ + public static Map readBaseItem(ResultSet set) throws SQLException { + Map item = new HashMap<>(); + item.put("id", set.getInt("id")); + item.put("sprite_id", set.getInt("sprite_id")); + item.put("item_name", set.getString("item_name")); + item.put("public_name", set.getString("public_name")); + item.put("type", set.getString("type")); + item.put("width", set.getInt("width")); + item.put("length", set.getInt("length")); + item.put("stack_height", set.getDouble("stack_height")); + item.put("allow_stack", set.getString("allow_stack")); + item.put("allow_walk", set.getString("allow_walk")); + item.put("allow_sit", set.getString("allow_sit")); + item.put("allow_lay", set.getString("allow_lay")); + item.put("interaction_type", set.getString("interaction_type")); + item.put("interaction_modes_count", set.getInt("interaction_modes_count")); + return item; + } + + /** + * Read all fields (14 base + 13 extended) from items_base into a Map. + */ + public static Map readFullItem(ResultSet set) throws SQLException { + Map item = readBaseItem(set); + item.put("allow_gift", set.getString("allow_gift")); + item.put("allow_trade", set.getString("allow_trade")); + item.put("allow_recycle", set.getString("allow_recycle")); + item.put("allow_marketplace_sell", set.getString("allow_marketplace_sell")); + item.put("allow_inventory_stack", set.getString("allow_inventory_stack")); + item.put("vending_ids", set.getString("vending_ids")); + item.put("customparams", set.getString("customparams")); + item.put("effect_id_male", set.getInt("effect_id_male")); + item.put("effect_id_female", set.getInt("effect_id_female")); + item.put("clothing_on_walk", set.getString("clothing_on_walk")); + item.put("multiheight", set.getString("multiheight")); + + // description may not exist in all schemas, handle gracefully + try { + item.put("description", set.getString("description")); + } catch (SQLException e) { + item.put("description", ""); + } + + return item; + } + + /** + * Read a catalog item reference from a result set that joined + * catalog_items with catalog_pages. + */ + public static Map readCatalogRef(ResultSet set) throws SQLException { + Map ref = new HashMap<>(); + ref.put("id", set.getInt("ci_id")); + ref.put("catalog_name", set.getString("catalog_name")); + ref.put("cost_credits", set.getInt("cost_credits")); + ref.put("cost_points", set.getInt("cost_points")); + ref.put("points_type", set.getInt("points_type")); + ref.put("page_id", set.getInt("ci_page_id")); + ref.put("page_caption", set.getString("page_caption")); + return ref; + } + + /** + * Whitelist of allowed field names for update operations. + * Prevents SQL injection via arbitrary column names. + */ + public static final java.util.Set ALLOWED_UPDATE_FIELDS = java.util.Set.of( + "item_name", "public_name", "sprite_id", "type", "width", "length", + "stack_height", "allow_stack", "allow_walk", "allow_sit", "allow_lay", + "allow_gift", "allow_trade", "allow_recycle", "allow_marketplace_sell", + "allow_inventory_stack", "interaction_type", "interaction_modes_count", + "vending_ids", "customparams", "effect_id_male", "effect_id_female", + "clothing_on_walk", "multiheight", "description" + ); + + /** + * Map camelCase JS field names to DB column names. + */ + public static final Map FIELD_MAP = Map.ofEntries( + Map.entry("itemName", "item_name"), + Map.entry("publicName", "public_name"), + Map.entry("spriteId", "sprite_id"), + Map.entry("type", "type"), + Map.entry("width", "width"), + Map.entry("length", "length"), + Map.entry("stackHeight", "stack_height"), + Map.entry("allowStack", "allow_stack"), + Map.entry("allowWalk", "allow_walk"), + Map.entry("allowSit", "allow_sit"), + Map.entry("allowLay", "allow_lay"), + Map.entry("allowGift", "allow_gift"), + Map.entry("allowTrade", "allow_trade"), + Map.entry("allowRecycle", "allow_recycle"), + Map.entry("allowMarketplaceSell", "allow_marketplace_sell"), + Map.entry("allowInventoryStack", "allow_inventory_stack"), + Map.entry("interactionType", "interaction_type"), + Map.entry("interactionModesCount", "interaction_modes_count"), + Map.entry("vendingIds", "vending_ids"), + Map.entry("customparams", "customparams"), + Map.entry("effectIdMale", "effect_id_male"), + Map.entry("effectIdFemale", "effect_id_female"), + Map.entry("clothingOnWalk", "clothing_on_walk"), + Map.entry("multiheight", "multiheight"), + Map.entry("description", "description") + ); +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorInteractionsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorInteractionsEvent.java new file mode 100644 index 00000000..78dbb0d9 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorInteractionsEvent.java @@ -0,0 +1,45 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorInteractionsComposer; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class FurniEditorInteractionsEvent extends MessageHandler { + + private static List cachedInteractions = null; + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No permission")); + return; + } + + if (cachedInteractions == null) { + synchronized (FurniEditorInteractionsEvent.class) { + if (cachedInteractions == null) { + List list = new ArrayList<>(); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + Statement stmt = connection.createStatement(); + ResultSet set = stmt.executeQuery("SELECT DISTINCT interaction_type FROM items_base WHERE interaction_type != '' ORDER BY interaction_type ASC")) { + while (set.next()) { + list.add(set.getString("interaction_type")); + } + } + cachedInteractions = Collections.unmodifiableList(list); + } + } + } + + this.client.sendResponse(new FurniEditorInteractionsComposer(cachedInteractions)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorSearchEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorSearchEvent.java new file mode 100644 index 00000000..bfdc229c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorSearchEvent.java @@ -0,0 +1,115 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorSearchComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class FurniEditorSearchEvent extends MessageHandler { + + private static final int PAGE_SIZE = 20; + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No permission")); + return; + } + + String query = this.packet.readString(); + String type = this.packet.readString(); + int page = this.packet.readInt(); + + // Input validation + if (query.length() > 100) { + query = query.substring(0, 100); + } + + if (page < 1) page = 1; + + int offset = (page - 1) * PAGE_SIZE; + + // Build WHERE clause + StringBuilder whereClause = new StringBuilder("WHERE 1=1"); + List params = new ArrayList<>(); + + if (!query.isEmpty()) { + // Try numeric match first (id or sprite_id) + boolean isNumeric = false; + try { + int numericQuery = Integer.parseInt(query); + isNumeric = true; + whereClause.append(" AND (id = ? OR sprite_id = ? OR item_name LIKE ? OR public_name LIKE ?)"); + params.add(numericQuery); + params.add(numericQuery); + params.add("%" + query + "%"); + params.add("%" + query + "%"); + } catch (NumberFormatException e) { + whereClause.append(" AND (item_name LIKE ? OR public_name LIKE ?)"); + params.add("%" + query + "%"); + params.add("%" + query + "%"); + } + } + + if (type != null && !type.isEmpty()) { + whereClause.append(" AND type = ?"); + params.add(type); + } + + // Count total + int total = 0; + String countSql = "SELECT COUNT(*) FROM items_base " + whereClause; + String dataSql = "SELECT * FROM items_base " + whereClause + " ORDER BY id ASC LIMIT ? OFFSET ?"; + + List> items = new ArrayList<>(); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + // Get total count + try (PreparedStatement stmt = connection.prepareStatement(countSql)) { + int idx = 1; + for (Object param : params) { + if (param instanceof Integer) { + stmt.setInt(idx++, (Integer) param); + } else { + stmt.setString(idx++, (String) param); + } + } + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + total = rs.getInt(1); + } + } + } + + // Get items page + try (PreparedStatement stmt = connection.prepareStatement(dataSql)) { + int idx = 1; + for (Object param : params) { + if (param instanceof Integer) { + stmt.setInt(idx++, (Integer) param); + } else { + stmt.setString(idx++, (String) param); + } + } + stmt.setInt(idx++, PAGE_SIZE); + stmt.setInt(idx, offset); + + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + items.add(FurniEditorHelper.readBaseItem(rs)); + } + } + } + } + + this.client.sendResponse(new FurniEditorSearchComposer(items, total, page)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdateEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdateEvent.java new file mode 100644 index 00000000..845395f5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdateEvent.java @@ -0,0 +1,110 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class FurniEditorUpdateEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No permission")); + return; + } + + int id = this.packet.readInt(); + String jsonFieldsStr = this.packet.readString(); + + if (id <= 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid item ID")); + return; + } + + JsonObject json; + try { + json = JsonParser.parseString(jsonFieldsStr).getAsJsonObject(); + } catch (Exception e) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid JSON data")); + return; + } + + if (json.size() == 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No fields to update")); + return; + } + + // Build dynamic UPDATE with whitelisted fields + StringBuilder setClauses = new StringBuilder(); + List values = new ArrayList<>(); + + for (Map.Entry entry : json.entrySet()) { + String jsKey = entry.getKey(); + String dbColumn = FurniEditorHelper.FIELD_MAP.get(jsKey); + + if (dbColumn == null || !FurniEditorHelper.ALLOWED_UPDATE_FIELDS.contains(dbColumn)) { + continue; // Skip unknown or disallowed fields + } + + if (setClauses.length() > 0) setClauses.append(", "); + setClauses.append("`").append(dbColumn).append("` = ?"); + + JsonElement val = entry.getValue(); + if (val.isJsonPrimitive()) { + if (val.getAsJsonPrimitive().isBoolean()) { + values.add(val.getAsBoolean() ? "1" : "0"); + } else if (val.getAsJsonPrimitive().isNumber()) { + // Check if it's a decimal number + String numStr = val.getAsString(); + if (numStr.contains(".")) { + values.add(val.getAsDouble()); + } else { + values.add(val.getAsInt()); + } + } else { + values.add(val.getAsString()); + } + } else { + values.add(val.toString()); + } + } + + if (setClauses.length() == 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No valid fields to update")); + return; + } + + String sql = "UPDATE items_base SET " + setClauses + " WHERE id = ?"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement stmt = connection.prepareStatement(sql)) { + int idx = 1; + for (Object value : values) { + if (value instanceof Integer) { + stmt.setInt(idx++, (Integer) value); + } else if (value instanceof Double) { + stmt.setDouble(idx++, (Double) value); + } else { + stmt.setString(idx++, String.valueOf(value)); + } + } + stmt.setInt(idx, id); + stmt.executeUpdate(); + } + + // Reload emulator item definitions + Emulator.getGameEnvironment().getItemManager().loadItems(); + + this.client.sendResponse(new FurniEditorResultComposer(true, "Item updated", id)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java index 3d28174e..f354973e 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java @@ -556,6 +556,12 @@ public class Outgoing { public static final int SnowStormUserRematchedComposer = 5029; + // Furni Editor + public static final int FurniEditorSearchComposer = 10040; + public static final int FurniEditorDetailComposer = 10041; + public static final int FurniEditorInteractionsComposer = 10043; + public static final int FurniEditorResultComposer = 10044; + // Catalog Admin public static final int CatalogAdminResultComposer = 10059; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorDetailComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorDetailComposer.java new file mode 100644 index 00000000..7c351af8 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorDetailComposer.java @@ -0,0 +1,77 @@ +package com.eu.habbo.messages.outgoing.furnieditor; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.List; +import java.util.Map; + +public class FurniEditorDetailComposer extends MessageComposer { + private final Map item; + private final int usageCount; + private final List> catalogItems; + private final String furniDataJson; + + public FurniEditorDetailComposer(Map item, int usageCount, List> catalogItems, String furniDataJson) { + this.item = item; + this.usageCount = usageCount; + this.catalogItems = catalogItems; + this.furniDataJson = furniDataJson; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.FurniEditorDetailComposer); + + // 14 base fields + this.response.appendInt((int) item.get("id")); + this.response.appendInt((int) item.get("sprite_id")); + this.response.appendString((String) item.getOrDefault("item_name", "")); + this.response.appendString((String) item.getOrDefault("public_name", "")); + this.response.appendString((String) item.getOrDefault("type", "s")); + this.response.appendInt((int) item.getOrDefault("width", 1)); + this.response.appendInt((int) item.getOrDefault("length", 1)); + this.response.appendDouble((double) item.getOrDefault("stack_height", 0.0)); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_stack", "1")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_walk", "0")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_sit", "0")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_lay", "0")))); + this.response.appendString((String) item.getOrDefault("interaction_type", "")); + this.response.appendInt((int) item.getOrDefault("interaction_modes_count", 0)); + + // 13 extended fields + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_gift", "1")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_trade", "1")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_recycle", "1")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_marketplace_sell", "1")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_inventory_stack", "1")))); + this.response.appendString((String) item.getOrDefault("vending_ids", "")); + this.response.appendString((String) item.getOrDefault("customparams", "")); + this.response.appendInt((int) item.getOrDefault("effect_id_male", 0)); + this.response.appendInt((int) item.getOrDefault("effect_id_female", 0)); + this.response.appendString((String) item.getOrDefault("clothing_on_walk", "")); + this.response.appendString((String) item.getOrDefault("multiheight", "")); + this.response.appendString((String) item.getOrDefault("description", "")); + + // usage count + this.response.appendInt(this.usageCount); + + // catalog references + this.response.appendInt(this.catalogItems.size()); + for (Map ci : this.catalogItems) { + this.response.appendInt((int) ci.get("id")); + this.response.appendString((String) ci.getOrDefault("catalog_name", "")); + this.response.appendInt((int) ci.getOrDefault("cost_credits", 0)); + this.response.appendInt((int) ci.getOrDefault("cost_points", 0)); + this.response.appendInt((int) ci.getOrDefault("points_type", 0)); + this.response.appendInt((int) ci.getOrDefault("page_id", -1)); + this.response.appendString((String) ci.getOrDefault("page_caption", "")); + } + + // furnidata JSON string + this.response.appendString(this.furniDataJson != null ? this.furniDataJson : "{}"); + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorInteractionsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorInteractionsComposer.java new file mode 100644 index 00000000..ea778e65 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorInteractionsComposer.java @@ -0,0 +1,27 @@ +package com.eu.habbo.messages.outgoing.furnieditor; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.List; + +public class FurniEditorInteractionsComposer extends MessageComposer { + private final List interactions; + + public FurniEditorInteractionsComposer(List interactions) { + this.interactions = interactions; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.FurniEditorInteractionsComposer); + this.response.appendInt(this.interactions.size()); + + for (String interaction : this.interactions) { + this.response.appendString(interaction); + } + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorResultComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorResultComposer.java new file mode 100644 index 00000000..07bc8e67 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorResultComposer.java @@ -0,0 +1,30 @@ +package com.eu.habbo.messages.outgoing.furnieditor; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class FurniEditorResultComposer extends MessageComposer { + private final boolean success; + private final String message; + private final int id; + + public FurniEditorResultComposer(boolean success, String message) { + this(success, message, -1); + } + + public FurniEditorResultComposer(boolean success, String message, int id) { + this.success = success; + this.message = message; + this.id = id; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.FurniEditorResultComposer); + this.response.appendBoolean(this.success); + this.response.appendString(this.message); + this.response.appendInt(this.id); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorSearchComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorSearchComposer.java new file mode 100644 index 00000000..c7ec1fcb --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorSearchComposer.java @@ -0,0 +1,50 @@ +package com.eu.habbo.messages.outgoing.furnieditor; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +public class FurniEditorSearchComposer extends MessageComposer { + private final List> items; + private final int total; + private final int page; + + public FurniEditorSearchComposer(List> items, int total, int page) { + this.items = items; + this.total = total; + this.page = page; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.FurniEditorSearchComposer); + this.response.appendInt(this.items.size()); + + for (Map item : this.items) { + this.response.appendInt((int) item.get("id")); + this.response.appendInt((int) item.get("sprite_id")); + this.response.appendString((String) item.getOrDefault("item_name", "")); + this.response.appendString((String) item.getOrDefault("public_name", "")); + this.response.appendString((String) item.getOrDefault("type", "s")); + this.response.appendInt((int) item.getOrDefault("width", 1)); + this.response.appendInt((int) item.getOrDefault("length", 1)); + this.response.appendDouble((double) item.getOrDefault("stack_height", 0.0)); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_stack", "1")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_walk", "0")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_sit", "0")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_lay", "0")))); + this.response.appendString((String) item.getOrDefault("interaction_type", "")); + this.response.appendInt((int) item.getOrDefault("interaction_modes_count", 0)); + } + + this.response.appendInt(this.total); + this.response.appendInt(this.page); + + return this.response; + } +}