Merge pull request #56 from duckietm/dev

Dev
This commit is contained in:
DuckieTM
2026-03-30 13:13:02 +02:00
committed by GitHub
16 changed files with 955 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
-- FurniEditor: emulator config keys for FurniDataManager
INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
('furni.editor.renderer.config.path', '\var\www\Gamedata\config\renderer-config.json'),
('furni.editor.asset.base.path', '\var\www\Gamedata\furniture\nitro-assets\');
@@ -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);
@@ -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;
@@ -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;
}
}
@@ -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);
}
}
@@ -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"));
}
}
@@ -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<String, Object> item = null;
int usageCount = 0;
List<Map<String, Object>> 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));
}
}
@@ -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<String, Object> readBaseItem(ResultSet set) throws SQLException {
Map<String, Object> 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<String, Object> readFullItem(ResultSet set) throws SQLException {
Map<String, Object> 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<String, Object> readCatalogRef(ResultSet set) throws SQLException {
Map<String, Object> 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<String> 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<String, String> 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")
);
}
@@ -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<String> 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<String> 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));
}
}
@@ -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<Object> 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<Map<String, Object>> 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));
}
}
@@ -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<Object> values = new ArrayList<>();
for (Map.Entry<String, JsonElement> 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));
}
}
@@ -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;
@@ -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<String, Object> item;
private final int usageCount;
private final List<Map<String, Object>> catalogItems;
private final String furniDataJson;
public FurniEditorDetailComposer(Map<String, Object> item, int usageCount, List<Map<String, Object>> 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<String, Object> 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;
}
}
@@ -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<String> interactions;
public FurniEditorInteractionsComposer(List<String> 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;
}
}
@@ -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;
}
}
@@ -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<Map<String, Object>> items;
private final int total;
private final int page;
public FurniEditorSearchComposer(List<Map<String, Object>> 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<String, Object> 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;
}
}