diff --git a/Database Updates/010_Wired_Update.sql b/Database Updates/010_Wired_Update.sql index daf373a6..60a92abf 100644 --- a/Database Updates/010_Wired_Update.sql +++ b/Database Updates/010_Wired_Update.sql @@ -91,7 +91,7 @@ ALTER TABLE `catalog_pages` 'builders_club_addons', 'builders_club_loyalty' ) NOT NULL DEFAULT 'default_3x3'; - + ALTER TABLE `catalog_pages` ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL','BUILDER','BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `club_only`; @@ -878,7 +878,7 @@ ON DUPLICATE KEY UPDATE `permission` = VALUES(`permission`), `overridable` = VALUES(`overridable`), `triggers_talking_furniture` = VALUES(`triggers_talking_furniture`); - + ALTER TABLE `catalog_club_offers` MODIFY COLUMN `type` ENUM('HC', 'VIP', 'BUILDERS_CLUB', 'BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC'; @@ -987,4 +987,3 @@ ALTER TABLE `catalog_pages_bc` 'builders_club_addons', 'builders_club_loyalty' ) NOT NULL DEFAULT 'default_3x3'; - diff --git a/Database Updates/016_custom_prefixes_setup.sql b/Database Updates/016_custom_prefixes_setup.sql index dd2b857a..3cbd5c6a 100644 --- a/Database Updates/016_custom_prefixes_setup.sql +++ b/Database Updates/016_custom_prefixes_setup.sql @@ -1,3 +1,13 @@ +-- ============================================================ +-- Custom Prefix System - Complete Setup (safe upgrade version) +-- ============================================================ + +-- Questo script è pensato per essere rieseguito senza errori +-- anche se le tabelle esistono già con una struttura parziale. + +-- ------------------------------------------------------------ +-- 1. Main user prefixes table +-- ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS `user_prefixes` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` INT(11) NOT NULL, @@ -5,28 +15,57 @@ CREATE TABLE IF NOT EXISTS `user_prefixes` ( `color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF', `icon` VARCHAR(50) NOT NULL DEFAULT '', `effect` VARCHAR(50) NOT NULL DEFAULT '', + `font` VARCHAR(50) NOT NULL DEFAULT '', + `catalog_prefix_id` INT(11) NOT NULL DEFAULT 0, + `display_name` VARCHAR(100) NOT NULL DEFAULT '', + `points` INT(11) NOT NULL DEFAULT 0, + `points_type` INT(11) NOT NULL DEFAULT 0, + `is_custom` TINYINT(1) NOT NULL DEFAULT 1, `active` TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), INDEX `idx_user_id` (`user_id`), INDEX `idx_user_active` (`user_id`, `active`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- 2. Prefix settings table +-- ------------------------------------------------------------ +-- 2. Catalog table +-- ------------------------------------------------------------ +CREATE TABLE IF NOT EXISTS `custom_prefixes_catalog` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `display_name` VARCHAR(100) NOT NULL DEFAULT '', + `text` VARCHAR(50) NOT NULL, + `color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF', + `icon` VARCHAR(50) NOT NULL DEFAULT '', + `effect` VARCHAR(50) NOT NULL DEFAULT '', + `font` VARCHAR(50) NOT NULL DEFAULT '', + `points` INT(11) NOT NULL DEFAULT 0, + `points_type` INT(11) NOT NULL DEFAULT 0, + `enabled` TINYINT(1) NOT NULL DEFAULT 1, + `sort_order` INT(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ------------------------------------------------------------ +-- 3. User visual settings +-- ------------------------------------------------------------ +CREATE TABLE IF NOT EXISTS `user_visual_settings` ( + `user_id` INT(11) NOT NULL, + `display_order` VARCHAR(50) NOT NULL DEFAULT 'icon-prefix-name', + PRIMARY KEY (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ------------------------------------------------------------ +-- 4. Prefix settings table +-- ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS `custom_prefix_settings` ( `key_name` VARCHAR(100) NOT NULL, `value` VARCHAR(255) NOT NULL, PRIMARY KEY (`key_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- Default settings -INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES - ('max_length', '15'), - ('min_rank_to_buy', '1'), - ('price_credits', '5'), - ('price_points', '0'), - ('points_type', '0'); - --- 3. Blacklisted words table +-- ------------------------------------------------------------ +-- 5. Blacklist table +-- ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `word` VARCHAR(100) NOT NULL, @@ -34,13 +73,249 @@ CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` ( UNIQUE KEY `uk_word` (`word`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- Example blacklist entries (customize as needed) +-- ============================================================ +-- Schema upgrades for existing installations +-- ============================================================ + +-- ------------------------------------------------------------ +-- user_prefixes: add missing columns safely +-- ------------------------------------------------------------ + +SET @dbname = DATABASE(); + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'user_prefixes' + AND COLUMN_NAME = 'font' + ), + 'SELECT 1', + 'ALTER TABLE `user_prefixes` ADD COLUMN `font` VARCHAR(50) NOT NULL DEFAULT '''' AFTER `effect`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'user_prefixes' + AND COLUMN_NAME = 'catalog_prefix_id' + ), + 'SELECT 1', + 'ALTER TABLE `user_prefixes` ADD COLUMN `catalog_prefix_id` INT(11) NOT NULL DEFAULT 0 AFTER `font`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'user_prefixes' + AND COLUMN_NAME = 'display_name' + ), + 'SELECT 1', + 'ALTER TABLE `user_prefixes` ADD COLUMN `display_name` VARCHAR(100) NOT NULL DEFAULT '''' AFTER `catalog_prefix_id`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'user_prefixes' + AND COLUMN_NAME = 'points' + ), + 'SELECT 1', + 'ALTER TABLE `user_prefixes` ADD COLUMN `points` INT(11) NOT NULL DEFAULT 0 AFTER `display_name`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'user_prefixes' + AND COLUMN_NAME = 'points_type' + ), + 'SELECT 1', + 'ALTER TABLE `user_prefixes` ADD COLUMN `points_type` INT(11) NOT NULL DEFAULT 0 AFTER `points`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'user_prefixes' + AND COLUMN_NAME = 'is_custom' + ), + 'SELECT 1', + 'ALTER TABLE `user_prefixes` ADD COLUMN `is_custom` TINYINT(1) NOT NULL DEFAULT 1 AFTER `points_type`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- ------------------------------------------------------------ +-- custom_prefixes_catalog: add missing columns safely +-- ------------------------------------------------------------ + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'custom_prefixes_catalog' + AND COLUMN_NAME = 'font' + ), + 'SELECT 1', + 'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `font` VARCHAR(50) NOT NULL DEFAULT '''' AFTER `effect`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'custom_prefixes_catalog' + AND COLUMN_NAME = 'points' + ), + 'SELECT 1', + 'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `points` INT(11) NOT NULL DEFAULT 0 AFTER `font`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'custom_prefixes_catalog' + AND COLUMN_NAME = 'points_type' + ), + 'SELECT 1', + 'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `points_type` INT(11) NOT NULL DEFAULT 0 AFTER `points`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'custom_prefixes_catalog' + AND COLUMN_NAME = 'enabled' + ), + 'SELECT 1', + 'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `enabled` TINYINT(1) NOT NULL DEFAULT 1 AFTER `points_type`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname + AND TABLE_NAME = 'custom_prefixes_catalog' + AND COLUMN_NAME = 'sort_order' + ), + 'SELECT 1', + 'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `sort_order` INT(11) NOT NULL DEFAULT 0 AFTER `enabled`' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- ============================================================ +-- Default settings +-- ============================================================ +INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES + ('max_length', '15'), + ('min_rank_to_buy', '1'), + ('price_credits', '5'), + ('price_points', '0'), + ('points_type', '0'), + ('font_price_credits', '10'), + ('font_price_points', '0'), + ('font_points_type', '0'); + +-- ============================================================ +-- Default catalog entries +-- ============================================================ +INSERT IGNORE INTO `custom_prefixes_catalog` +(`id`, `display_name`, `text`, `color`, `icon`, `effect`, `font`, `points`, `points_type`, `enabled`, `sort_order`) VALUES + (1, 'VIP', 'VIP', '#FFD700', '', 'glow', '', 10, 0, 1, 1), + (2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2), + (3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3); + +-- ============================================================ +-- Example blacklist entries +-- ============================================================ INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES ('admin'), ('staff'), ('mod'), ('owner'); +-- ============================================================ +-- Notes +-- ============================================================ +-- Preset prefixes for `:customize` are loaded directly by +-- UserNickIconsComposer and displayed inside the `:customize` panel. +-- +-- This setup does not require rows in `catalog_pages`. +-- +-- Command texts / permission inserts are intentionally omitted +-- for compatibility with both legacy and normalized permission schemas. + INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES -- GivePrefix command ('commands.keys.cmd_give_prefix', 'giveprefix'), @@ -79,4 +354,3 @@ VALUES ('cmd_list_prefixes', '1', '0', '0', '0', '0', '0', '0', '1'), ('cmd_remove_prefix', '1', '0', '0', '0', '0', '0', '0', '1'), ('cmd_prefix_blacklist', '1', '0', '0', '0', '0', '0', '0', '1'); - diff --git a/Database Updates/custom_nick_icons_setup.sql b/Database Updates/custom_nick_icons_setup.sql new file mode 100644 index 00000000..dbf9f81a --- /dev/null +++ b/Database Updates/custom_nick_icons_setup.sql @@ -0,0 +1,36 @@ +-- ============================================================ +-- Nick Icon Customization Setup +-- ============================================================ + +CREATE TABLE IF NOT EXISTS `custom_nick_icons_catalog` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `icon_key` VARCHAR(50) NOT NULL, + `display_name` VARCHAR(100) NOT NULL DEFAULT '', + `points` INT(11) NOT NULL DEFAULT 0, + `points_type` INT(11) NOT NULL DEFAULT 0, + `enabled` TINYINT(1) NOT NULL DEFAULT 1, + `sort_order` INT(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_icon_key` (`icon_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `user_nick_icons` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `user_id` INT(11) NOT NULL, + `icon_key` VARCHAR(50) NOT NULL, + `active` TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_icon` (`user_id`, `icon_key`), + KEY `idx_user_id` (`user_id`), + KEY `idx_user_active` (`user_id`, `active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT IGNORE INTO `custom_nick_icons_catalog` (`icon_key`, `display_name`, `points`, `points_type`, `enabled`, `sort_order`) VALUES + ('1', 'Icon 1', 10, 0, 1, 1), + ('2', 'Icon 2', 10, 0, 1, 2), + ('3', 'Icon 3', 10, 0, 1, 3), + ('4', 'Icon 4', 10, 0, 1, 4), + ('5', 'Icon 5', 10, 0, 1, 5), + ('6', 'Icon 6', 10, 0, 1, 6); +ALTER TABLE `custom_nick_icons_catalog` + ADD COLUMN IF NOT EXISTS `display_name` VARCHAR(100) NOT NULL DEFAULT '' AFTER `icon_key`; diff --git a/Database Updates/remember_login_tokens.sql b/Database Updates/remember_login_tokens.sql new file mode 100644 index 00000000..770ec27f --- /dev/null +++ b/Database Updates/remember_login_tokens.sql @@ -0,0 +1,8 @@ +ALTER TABLE `users` + ADD COLUMN IF NOT EXISTS `remember_token_hash` VARCHAR(64) NOT NULL DEFAULT '' AFTER `auth_ticket`; + +ALTER TABLE `users` + ADD COLUMN IF NOT EXISTS `remember_token_expires_at` INT(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `remember_token_hash`; + +ALTER TABLE `users` + ADD INDEX IF NOT EXISTS `idx_users_remember_token_hash` (`remember_token_hash`); diff --git a/Database Updates/wired_message_length_512.sql b/Database Updates/wired_message_length_512.sql new file mode 100644 index 00000000..ad23b30f --- /dev/null +++ b/Database Updates/wired_message_length_512.sql @@ -0,0 +1,3 @@ +INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`) +VALUES ('hotel.wired.message.max_length', '512', 'Maximum length of text fields used by wired messages and bot text effects.') +ON DUPLICATE KEY UPDATE `value` = '512'; diff --git a/Emulator/pom.xml b/Emulator/pom.xml index db945291..03f1c329 100644 --- a/Emulator/pom.xml +++ b/Emulator/pom.xml @@ -6,7 +6,7 @@ com.eu.habbo Habbo - 4.1.2 + 4.1.13 UTF-8 @@ -163,7 +163,7 @@ 2.13.0 - org.mindrot @@ -171,7 +171,7 @@ 0.4 - org.eclipse.angus diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java index d8eec628..d8b8fe96 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java @@ -21,6 +21,7 @@ import com.eu.habbo.habbohotel.pets.PetManager; import com.eu.habbo.habbohotel.polls.PollManager; import com.eu.habbo.habbohotel.rooms.RoomChatBubbleManager; import com.eu.habbo.habbohotel.rooms.RoomManager; +import com.eu.habbo.habbohotel.translations.GoogleTranslateManager; import com.eu.habbo.habbohotel.users.HabboManager; import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeManager; import com.eu.habbo.habbohotel.users.infostand.InfostandBackgroundManager; @@ -60,6 +61,7 @@ public class GameEnvironment { private SubscriptionManager subscriptionManager; private CalendarManager calendarManager; private RoomChatBubbleManager roomChatBubbleManager; + private GoogleTranslateManager googleTranslateManager; private CustomBadgeManager customBadgeManager; private InfostandBackgroundManager infostandBackgroundManager; @@ -88,6 +90,7 @@ public class GameEnvironment { this.pollManager = new PollManager(); this.calendarManager = new CalendarManager(); this.roomChatBubbleManager = new RoomChatBubbleManager(); + this.googleTranslateManager = new GoogleTranslateManager(); this.customBadgeManager = new CustomBadgeManager(); this.infostandBackgroundManager = new InfostandBackgroundManager(); @@ -127,6 +130,9 @@ public class GameEnvironment { this.hotelViewManager.dispose(); this.subscriptionManager.dispose(); this.calendarManager.dispose(); + if (this.googleTranslateManager != null) { + this.googleTranslateManager.clearCache(); + } LOGGER.info("GameEnvironment -> Disposed!"); } @@ -226,6 +232,10 @@ public class GameEnvironment { return roomChatBubbleManager; } + public GoogleTranslateManager getGoogleTranslateManager() { + return this.googleTranslateManager; + } + public CustomBadgeManager getCustomBadgeManager() { return this.customBadgeManager; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java index 36712b0e..7e5ecdb8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java @@ -1,7 +1,6 @@ package com.eu.habbo.habbohotel.achievements; import com.eu.habbo.Emulator; -import com.eu.habbo.database.SqlQueries; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboBadge; @@ -50,12 +49,16 @@ public class AchievementManager { if (habbo != null) { progressAchievement(habbo, achievement, amount); } else { - try { - SqlQueries.update( - "INSERT INTO users_achievements_queue (user_id, achievement_id, amount) VALUES (?, ?, ?) " - + "ON DUPLICATE KEY UPDATE amount = amount + ?", - habboId, achievement.id, amount, amount); - } catch (SqlQueries.DataAccessException e) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("" + + "INSERT INTO users_achievements_queue (user_id, achievement_id, amount) VALUES (?, ?, ?) " + + "ON DUPLICATE KEY UPDATE amount = amount + ?")) { + statement.setInt(1, habboId); + statement.setInt(2, achievement.id); + statement.setInt(3, amount); + statement.setInt(4, amount); + statement.execute(); + } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); } } @@ -200,41 +203,48 @@ public class AchievementManager { } public static void createUserEntry(Habbo habbo, Achievement achievement) { - try { - SqlQueries.update( - "INSERT INTO users_achievements (user_id, achievement_name, progress) VALUES (?, ?, ?)", - habbo.getHabboInfo().getId(), achievement.name, 1); - } catch (SqlQueries.DataAccessException e) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_achievements (user_id, achievement_name, progress) VALUES (?, ?, ?)")) { + statement.setInt(1, habbo.getHabboInfo().getId()); + statement.setString(2, achievement.name); + statement.setInt(3, 1); + statement.execute(); + } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); } } public static void saveAchievements(Habbo habbo) { - int userId = habbo.getHabboInfo().getId(); - try { - SqlQueries.batchUpdate( - "UPDATE users_achievements SET progress = ? WHERE achievement_name = ? AND user_id = ? LIMIT 1", - habbo.getHabboStats().getAchievementProgress().entrySet(), - (ps, entry) -> { - ps.setInt(1, entry.getValue()); - ps.setString(2, entry.getKey().name); - ps.setInt(3, userId); - }); - } catch (SqlQueries.DataAccessException e) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users_achievements SET progress = ? WHERE achievement_name = ? AND user_id = ? LIMIT 1")) { + statement.setInt(3, habbo.getHabboInfo().getId()); + for (Map.Entry map : habbo.getHabboStats().getAchievementProgress().entrySet()) { + statement.setInt(1, map.getValue()); + statement.setString(2, map.getKey().name); + statement.addBatch(); + } + statement.executeBatch(); + } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); } } public static int getAchievementProgressForHabbo(int userId, Achievement achievement) { - try { - return SqlQueries.queryOne( - "SELECT progress FROM users_achievements WHERE user_id = ? AND achievement_name = ? LIMIT 1", - rs -> rs.getInt("progress"), - userId, achievement.name).orElse(0); - } catch (SqlQueries.DataAccessException e) { - LOGGER.error("Caught SQL exception", e); + if (achievement == null) { return 0; } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT progress FROM users_achievements WHERE user_id = ? AND achievement_name = ? LIMIT 1")) { + statement.setInt(1, userId); + statement.setString(2, achievement.name); + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + return set.getInt("progress"); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + + return 0; } public void reload() { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java index 6c0907ff..80f820d3 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java @@ -71,13 +71,23 @@ public class BotManager { } public Bot createBot(THashMap data, String type) { + return this.createBot(data, type, 0); + } + + public Bot createBot(THashMap data, String type, int ownerId) { + if (ownerId <= 0) { + LOGGER.error("Cannot create bot of type '{}' without a valid owner user id.", type); + return null; + } + Bot bot = null; - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO bots (user_id, room_id, name, motto, figure, gender, type) VALUES (0, 0, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { - statement.setString(1, data.get("name")); - statement.setString(2, data.get("motto")); - statement.setString(3, data.get("figure")); - statement.setString(4, data.get("gender").toUpperCase()); - statement.setString(5, type); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO bots (user_id, room_id, name, motto, figure, gender, type) VALUES (?, 0, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { + statement.setInt(1, ownerId); + statement.setString(2, data.get("name")); + statement.setString(3, data.get("motto")); + statement.setString(4, data.get("figure")); + statement.setString(5, data.get("gender").toUpperCase()); + statement.setString(6, type); statement.execute(); try (ResultSet set = statement.getGeneratedKeys()) { if (set.next()) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java index 3d45686b..8ac07faf 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java @@ -1058,7 +1058,7 @@ public class CatalogManager { } } - Bot bot = Emulator.getGameEnvironment().getBotManager().createBot(data, type); + Bot bot = Emulator.getGameEnvironment().getBotManager().createBot(data, type, habbo.getHabboInfo().getId()); if (bot != null) { bot.setOwnerId(habbo.getClient().getHabbo().getHabboInfo().getId()); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java index 590ea9d8..5ed0e938 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java @@ -1,5 +1,6 @@ package com.eu.habbo.habbohotel.items.interactions.wired.conditions; +import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; @@ -11,10 +12,12 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.ServerMessage; +import gnu.trove.set.hash.THashSet; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; +import java.util.stream.Collectors; public class WiredConditionSelectionQuantity extends InteractionWiredCondition { private static final int COMPARISON_LESS_THAN = 0; @@ -23,9 +26,16 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { private static final int SOURCE_GROUP_USERS = 0; private static final int SOURCE_GROUP_FURNI = 1; + private static final int SOURCE_USER_TRIGGER = 0; + private static final int SOURCE_USER_SIGNAL = 1; + private static final int SOURCE_USER_CLICKED = 2; + private static final int SOURCE_FURNI_TRIGGER = 3; + private static final int SOURCE_FURNI_PICKED = 4; + private static final int SOURCE_FURNI_SIGNAL = 5; public static final WiredConditionType type = WiredConditionType.SLC_QUANTITY; + private final THashSet items; private int comparison = COMPARISON_EQUAL; private int quantity = 0; private int sourceGroup = SOURCE_GROUP_USERS; @@ -33,10 +43,12 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { public WiredConditionSelectionQuantity(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); + this.items = new THashSet<>(); } public WiredConditionSelectionQuantity(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { super(id, userId, item, extradata, limitedStack, limitedSells); + this.items = new THashSet<>(); } @Override @@ -46,9 +58,18 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { @Override public void serializeWiredData(ServerMessage message, Room room) { - message.appendBoolean(false); - message.appendInt(5); - message.appendInt(0); + this.refresh(room); + + boolean pickMode = this.sourceGroup == SOURCE_GROUP_FURNI && this.sourceType == WiredSourceUtil.SOURCE_SELECTED; + + message.appendBoolean(pickMode); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(pickMode ? this.items.size() : 0); + if (pickMode) { + for (HabboItem item : this.items) { + message.appendInt(item.getId()); + } + } message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); @@ -69,8 +90,36 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL; this.quantity = (params.length > 1) ? this.normalizeQuantity(params[1]) : 0; - this.sourceGroup = (params.length > 2) ? this.normalizeSourceGroup(params[2]) : SOURCE_GROUP_USERS; - this.sourceType = (params.length > 3) ? this.normalizeSourceType(this.sourceGroup, params[3]) : WiredSourceUtil.SOURCE_TRIGGER; + this.items.clear(); + + if (params.length > 3) { + this.sourceGroup = this.normalizeSourceGroup(params[2]); + this.sourceType = this.normalizeSourceType(this.sourceGroup, params[3]); + } else { + this.setSourceSelection((params.length > 2) ? params[2] : SOURCE_USER_TRIGGER); + } + + if (this.sourceGroup != SOURCE_GROUP_FURNI || this.sourceType != WiredSourceUtil.SOURCE_SELECTED) { + return true; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + return false; + } + + int count = settings.getFurniIds().length; + if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + return false; + } + + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + this.items.add(item); + } + } return true; } @@ -97,11 +146,14 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { @Override public String getWiredData() { + this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId())); + return WiredManager.getGson().toJson(new JsonData( this.comparison, this.quantity, this.sourceGroup, - this.sourceType + this.sourceType, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) )); } @@ -125,6 +177,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { this.quantity = this.normalizeQuantity(data.quantity); this.sourceGroup = this.normalizeSourceGroup(data.sourceGroup); this.sourceType = this.normalizeSourceType(this.sourceGroup, data.sourceType); + this.loadSelectedItems(data.itemIds, room); return; } @@ -150,6 +203,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { @Override public void onPickUp() { + this.items.clear(); this.comparison = COMPARISON_EQUAL; this.quantity = 0; this.sourceGroup = SOURCE_GROUP_USERS; @@ -158,7 +212,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { private int resolveCount(WiredContext ctx) { if (this.sourceGroup == SOURCE_GROUP_FURNI) { - List items = WiredSourceUtil.resolveItems(ctx, this.sourceType, null); + List items = WiredSourceUtil.resolveItems(ctx, this.sourceType, this.items); return items.size(); } @@ -188,10 +242,18 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { private int normalizeSourceType(int group, int value) { if (group == SOURCE_GROUP_USERS) { - return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + switch (value) { + case WiredSourceUtil.SOURCE_CLICKED_USER: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_SELECTOR: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } } switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: case WiredSourceUtil.SOURCE_SELECTOR: case WiredSourceUtil.SOURCE_SIGNAL: case WiredSourceUtil.SOURCE_TRIGGER: @@ -201,17 +263,104 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition { } } + private int getSourceSelection() { + if (this.sourceGroup == SOURCE_GROUP_FURNI) { + switch (this.sourceType) { + case WiredSourceUtil.SOURCE_SELECTED: + return SOURCE_FURNI_PICKED; + case WiredSourceUtil.SOURCE_SIGNAL: + return SOURCE_FURNI_SIGNAL; + default: + return SOURCE_FURNI_TRIGGER; + } + } + + switch (this.sourceType) { + case WiredSourceUtil.SOURCE_CLICKED_USER: + return SOURCE_USER_CLICKED; + case WiredSourceUtil.SOURCE_SIGNAL: + return SOURCE_USER_SIGNAL; + default: + return SOURCE_USER_TRIGGER; + } + } + + private void setSourceSelection(int value) { + switch (value) { + case SOURCE_USER_SIGNAL: + this.sourceGroup = SOURCE_GROUP_USERS; + this.sourceType = WiredSourceUtil.SOURCE_SIGNAL; + break; + case SOURCE_USER_CLICKED: + this.sourceGroup = SOURCE_GROUP_USERS; + this.sourceType = WiredSourceUtil.SOURCE_CLICKED_USER; + break; + case SOURCE_FURNI_TRIGGER: + this.sourceGroup = SOURCE_GROUP_FURNI; + this.sourceType = WiredSourceUtil.SOURCE_TRIGGER; + break; + case SOURCE_FURNI_PICKED: + this.sourceGroup = SOURCE_GROUP_FURNI; + this.sourceType = WiredSourceUtil.SOURCE_SELECTED; + break; + case SOURCE_FURNI_SIGNAL: + this.sourceGroup = SOURCE_GROUP_FURNI; + this.sourceType = WiredSourceUtil.SOURCE_SIGNAL; + break; + default: + this.sourceGroup = SOURCE_GROUP_USERS; + this.sourceType = WiredSourceUtil.SOURCE_TRIGGER; + break; + } + } + + private void loadSelectedItems(List itemIds, Room room) { + this.items.clear(); + + if (itemIds == null || room == null) { + return; + } + + for (Integer itemId : itemIds) { + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + this.items.add(item); + } + } + } + + private void refresh(Room room) { + if (room == null || this.items.isEmpty()) { + return; + } + + THashSet itemsToRemove = new THashSet<>(); + + for (HabboItem item : this.items) { + if (item == null || room.getHabboItem(item.getId()) == null) { + itemsToRemove.add(item); + } + } + + for (HabboItem item : itemsToRemove) { + this.items.remove(item); + } + } + static class JsonData { int comparison; int quantity; int sourceGroup; int sourceType; + List itemIds; - public JsonData(int comparison, int quantity, int sourceGroup, int sourceType) { + public JsonData(int comparison, int quantity, int sourceGroup, int sourceType, List itemIds) { this.comparison = comparison; this.quantity = quantity; this.sourceGroup = sourceGroup; this.sourceType = sourceType; + this.itemIds = itemIds; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java index d027210b..b4f20a8f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java @@ -82,7 +82,7 @@ public class WiredEffectBotTalk extends InteractionWiredEffect { this.setDelay(delay); this.botName = data[0].substring(0, Math.min(data[0].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100))); - this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100))); + this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.bot.message.max_length", 100))); this.mode = mode; return true; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java index 2673993c..c3bc7dfe 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java @@ -105,7 +105,7 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { throw new WiredSaveException("Delay too long"); this.botName = data[0].substring(0, Math.min(data[0].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100))); - this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100))); + this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.bot.message.max_length", 100))); this.mode = mode; this.setDelay(delay); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectControlClock.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectControlClock.java index b4f55831..92a8a1f7 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectControlClock.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectControlClock.java @@ -4,6 +4,7 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameTimer; import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameUpCounter; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; @@ -60,29 +61,74 @@ public class WiredEffectControlClock extends InteractionWiredEffect { } for (HabboItem item : effectiveItems) { - if (!(item instanceof InteractionGameUpCounter)) { + if (!(item instanceof InteractionGameTimer)) { continue; } - InteractionGameUpCounter counter = (InteractionGameUpCounter) item; - - switch (this.action) { - case ACTION_START: - counter.restartFromZero(room); - break; - case ACTION_STOP: - counter.stopCounter(room); - break; - case ACTION_RESET: - counter.resetCounter(room); - break; - case ACTION_PAUSE: - counter.pauseCounter(room); - break; - case ACTION_RESUME: - counter.resumeCounter(room); - break; + if (item instanceof InteractionGameUpCounter) { + this.controlUpCounter((InteractionGameUpCounter) item, room); + continue; } + + this.controlGameTimer((InteractionGameTimer) item, room); + } + } + + private void controlUpCounter(InteractionGameUpCounter counter, Room room) { + switch (this.action) { + case ACTION_START: + counter.restartFromZero(room); + break; + case ACTION_STOP: + counter.stopCounter(room); + break; + case ACTION_RESET: + counter.resetCounter(room); + break; + case ACTION_PAUSE: + counter.pauseCounter(room); + break; + case ACTION_RESUME: + counter.resumeCounter(room); + break; + } + } + + private void controlGameTimer(InteractionGameTimer timer, Room room) { + switch (this.action) { + case ACTION_START: + timer.startTimer(room); + break; + case ACTION_STOP: + this.stopGameTimer(timer, room, false); + break; + case ACTION_RESET: + this.stopGameTimer(timer, room, true); + break; + case ACTION_PAUSE: + timer.pauseTimer(room); + break; + case ACTION_RESUME: + timer.resumeTimer(room); + break; + } + } + + private void stopGameTimer(InteractionGameTimer timer, Room room, boolean resetTime) { + boolean wasActive = timer.isRunning() || timer.isPaused(); + + timer.endGame(room); + + if (resetTime) { + timer.setTimeNow(timer.getBaseTime()); + timer.setExtradata(timer.getTimeNow() + "\t" + timer.getBaseTime()); + } + + room.updateItem(timer); + timer.needsUpdate(true); + + if (wasActive) { + WiredManager.triggerGameEnds(room); } } @@ -206,7 +252,7 @@ public class WiredEffectControlClock extends InteractionWiredEffect { throw new WiredSaveException(String.format("Item %s not found", itemId)); } - if (!(item instanceof InteractionGameUpCounter)) { + if (!(item instanceof InteractionGameTimer)) { throw new WiredSaveException("wiredfurni.error.require_counter_furni"); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToFurni.java index ed8bf0a9..265ad507 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToFurni.java @@ -53,26 +53,37 @@ public class WiredEffectFurniToFurni extends InteractionWiredEffect { return; } - HabboItem moveItem = this.resolveLastMoveItem(ctx); - HabboItem targetItem = this.resolveLastTargetItem(ctx); + List moveItems = this.resolveMoveItems(ctx); + List targetItems = this.resolveTargetItems(ctx); - if (moveItem == null || targetItem == null || moveItem.getId() == targetItem.getId()) { + if (moveItems.isEmpty() || targetItems.isEmpty()) { return; } - RoomTile targetTile = room.getLayout().getTile(targetItem.getX(), targetItem.getY()); - if (targetTile == null) { - return; - } + int targetIndex = 0; + for (HabboItem moveItem : moveItems) { + if (moveItem == null) { + continue; + } - FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), null, false, ctx); - if (error == FurnitureMovementError.NONE) { - return; - } + HabboItem targetItem = targetItems.get(targetIndex % targetItems.size()); + targetIndex++; - error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), targetItem.getZ(), null, false, ctx); - if (error == FurnitureMovementError.NONE) { - return; + if (targetItem == null || moveItem.getId() == targetItem.getId()) { + continue; + } + + RoomTile targetTile = room.getLayout().getTile(targetItem.getX(), targetItem.getY()); + if (targetTile == null) { + continue; + } + + FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), null, false, ctx); + if (error == FurnitureMovementError.NONE) { + continue; + } + + WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), targetItem.getZ(), null, false, ctx); } } @@ -233,35 +244,23 @@ public class WiredEffectFurniToFurni extends InteractionWiredEffect { return COOLDOWN_MOVEMENT; } - private HabboItem resolveLastMoveItem(WiredContext ctx) { - return this.resolveLastItem(ctx, this.moveSource, this.moveItems); + private List resolveMoveItems(WiredContext ctx) { + return this.resolveItems(ctx, this.moveSource, this.moveItems); } - private HabboItem resolveLastTargetItem(WiredContext ctx) { + private List resolveTargetItems(WiredContext ctx) { int source = (this.targetSource == SOURCE_SECONDARY_SELECTED) ? WiredSourceUtil.SOURCE_SELECTED : this.targetSource; - return this.resolveLastItem(ctx, source, this.targetItems); + return this.resolveItems(ctx, source, this.targetItems); } - private HabboItem resolveLastItem(WiredContext ctx, int source, List items) { + private List resolveItems(WiredContext ctx, int source, List items) { if (source == WiredSourceUtil.SOURCE_SELECTED) { this.validateItems(items); } - List resolvedItems = WiredSourceUtil.resolveItems(ctx, source, items); - - if (resolvedItems.isEmpty()) { - return null; - } - - for (int index = resolvedItems.size() - 1; index >= 0; index--) { - HabboItem item = resolvedItems.get(index); - - if (item != null) { - return item; - } - } - - return null; + return WiredSourceUtil.resolveItems(ctx, source, items).stream() + .filter(item -> item != null && ctx.room().getHabboItem(item.getId()) != null) + .collect(Collectors.toList()); } private List parseItems(String data, Room room) throws WiredSaveException { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java index f9121ce5..99a14b67 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java @@ -33,7 +33,7 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { public static final WiredEffectType type = WiredEffectType.SEND_SIGNAL; - private static final int MAX_SIGNAL_DEPTH = 10; + public static int MAX_SIGNAL_DEPTH = 100; private static final int ANTENNA_PICKED = 0; private static final int ANTENNA_TRIGGER = 1; @@ -166,7 +166,7 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { .signalChannel(signalChannel) .signalUserCount(signalUserCount) .signalFurniCount(sourceItem != null ? 1 : 0) - .contextVariableScope(ctx.contextVariables()) + .contextVariableScope(ctx.contextVariables().copy()) .triggeredByEffect(true); if (actor != null) builder.actor(actor); @@ -286,15 +286,6 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { } } - if (room != null && room.getRoomSpecialTypes() != null) { - for (HabboItem receiver : newItems) { - int count = room.getRoomSpecialTypes().countSendersTargetingReceiver(receiver.getId(), this); - if (count >= RoomSpecialTypes.MAX_SENDERS_PER_RECEIVER) { - throw new WiredSaveException("Maximum of " + RoomSpecialTypes.MAX_SENDERS_PER_RECEIVER + " senders per receiver reached"); - } - } - } - int delay = settings.getDelay(); if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { throw new WiredSaveException("Delay too long"); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java index 35e957c4..a2b858f4 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java @@ -34,6 +34,8 @@ public class WiredEffectWhisper extends InteractionWiredEffect { private static final long DELIVERY_DEDUP_TTL_MS = 60_000L; private static final int DELIVERY_DEDUP_CLEANUP_THRESHOLD = 512; private static final ConcurrentHashMap DELIVERY_DEDUP = new ConcurrentHashMap<>(); + private static final int DEFAULT_SHOW_MESSAGE_MAX_LENGTH = 200; + private static final int DEFAULT_SHOW_MESSAGE_MAX_LINES = 8; protected String message = ""; protected int userSource = WiredSourceUtil.SOURCE_TRIGGER; @@ -96,9 +98,12 @@ public class WiredEffectWhisper extends InteractionWiredEffect { if(gameClient.getHabbo() == null || !gameClient.getHabbo().hasPermission(Permission.ACC_SUPERWIRED)) { message = Emulator.getGameEnvironment().getWordFilter().filter(message, null); - message = message.substring(0, Math.min(message.length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100))); } + int maxLength = Emulator.getConfig().getInt("hotel.wired.show_message.max_length", DEFAULT_SHOW_MESSAGE_MAX_LENGTH); + int maxLines = Emulator.getConfig().getInt("hotel.wired.show_message.max_lines", DEFAULT_SHOW_MESSAGE_MAX_LINES); + message = clampMessage(message, maxLength, maxLines); + int delay = settings.getDelay(); if(delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) @@ -109,6 +114,35 @@ public class WiredEffectWhisper extends InteractionWiredEffect { return true; } + private static String clampMessage(String value, int maxLength, int maxLines) { + if (value == null || value.isEmpty()) { + return ""; + } + + int safeMaxLength = Math.max(1, maxLength); + int safeMaxLines = Math.max(1, maxLines); + + String normalized = value.replace("\r\n", "\n").replace('\r', '\n'); + String[] lines = normalized.split("\n", -1); + + StringBuilder builder = new StringBuilder(); + int linesToWrite = Math.min(lines.length, safeMaxLines); + + for (int index = 0; index < linesToWrite; index++) { + if (builder.length() > 0) { + builder.append('\n'); + } + + builder.append(lines[index]); + } + + if (builder.length() > safeMaxLength) { + builder.setLength(safeMaxLength); + } + + return builder.toString(); + } + protected List resolveUsers(WiredContext ctx) { return WiredSourceUtil.resolveUsers(ctx, this.userSource); } @@ -212,7 +246,9 @@ public class WiredEffectWhisper extends InteractionWiredEffect { } String msg = buildMessage(ctx, (sharedSourceHabbo != null) ? sharedSourceHabbo : habbo); - habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(msg, habbo, habbo, RoomChatMessageBubbles.getBubble(this.bubbleStyle)))); + habbo.getClient().sendResponse(new RoomUserWhisperComposer( + new RoomChatMessage(msg, habbo.getRoomUnit(), RoomChatMessageBubbles.getBubble(this.bubbleStyle)) + )); if (habbo.getRoomUnit().isIdle()) { habbo.getRoomUnit().getRoom().unIdle(habbo); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java index 504abe1c..89aa8f0e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java @@ -170,13 +170,15 @@ public class WiredExtraTextInputVariable extends InteractionWiredExtra { } public Integer resolveCapturedValue(Room room, String rawValue) { - String normalizedValue = rawValue != null ? rawValue.trim() : ""; - if (normalizedValue.isEmpty()) { - return null; - } + String capturedValue = rawValue != null ? rawValue : ""; + String normalizedValue = capturedValue.trim(); if (this.getDisplayType(room) == DISPLAY_TEXTUAL) { - return WiredVariableTextConnectorSupport.toValue(room, this.variableItemId, normalizedValue); + return WiredVariableTextConnectorSupport.toValue(room, this.variableItemId, capturedValue); + } + + if (normalizedValue.isEmpty()) { + return null; } try { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java index 96149a5d..2dda3c39 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java @@ -22,6 +22,7 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { public static final int CODE = 79; public static final int MAX_MAPPING_LENGTH = 1000; public static final int MAX_MAPPING_LINES = 30; + private static final String PRESERVED_SPACE = "\u00A0"; private String mappingsText = ""; private LinkedHashMap mappings = new LinkedHashMap<>(); @@ -123,8 +124,12 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { return ""; } - String mappedValue = this.mappings.get(value); - return mappedValue != null ? mappedValue : String.valueOf(value); + if (this.mappings.containsKey(value)) { + String mappedValue = this.mappings.get(value); + return mappedValue != null ? preserveSpaces(mappedValue) : ""; + } + + return String.valueOf(value); } public Integer resolveValue(String text) { @@ -132,17 +137,16 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { return null; } - String normalizedText = text.trim(); - if (normalizedText.isEmpty()) { - return null; - } + String normalizedText = normalizePreservedSpaces(text); for (Map.Entry entry : this.mappings.entrySet()) { if (entry == null || entry.getKey() == null || entry.getValue() == null) { continue; } - if (entry.getValue().trim().equalsIgnoreCase(normalizedText)) { + String normalizedMappingValue = normalizePreservedSpaces(entry.getValue()); + + if (normalizedMappingValue.equalsIgnoreCase(normalizedText)) { return entry.getKey(); } } @@ -195,8 +199,8 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { continue; } - String line = rawLine.trim(); - if (line.isEmpty()) { + String line = rawLine; + if (line.trim().isEmpty()) { continue; } @@ -210,7 +214,7 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { } String keyPart = line.substring(0, separatorIndex).trim(); - String valuePart = line.substring(separatorIndex + 1).trim(); + String valuePart = line.substring(separatorIndex + 1); try { result.put(Integer.parseInt(keyPart), valuePart); @@ -221,6 +225,14 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra { return result; } + private static String preserveSpaces(String value) { + return value.replace(" ", PRESERVED_SPACE); + } + + private static String normalizePreservedSpaces(String value) { + return value.replace(PRESERVED_SPACE, " "); + } + static class JsonData { String mappingsText; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniAltitude.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniAltitude.java index b77a2c1c..24029d04 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniAltitude.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniAltitude.java @@ -95,6 +95,11 @@ public class WiredEffectFurniAltitude extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniArea.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniArea.java index 7abbc94a..2296c9a5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniArea.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniArea.java @@ -100,6 +100,11 @@ public class WiredEffectFurniArea extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java index 02c2630d..dd4f73c4 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java @@ -155,6 +155,11 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson( diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java index 8ee8a944..8e175a25 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java @@ -38,6 +38,7 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { private static final int MAX_PICKED_FURNI = 20; private static final int MAX_TILE_OFFSETS = 64; + private static final int GRID_RANGE = 4; private int sourceType = SOURCE_USER_TRIGGER; private boolean filterExisting = false; @@ -69,8 +70,20 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { int totalRaw = 0; int wiredSkipped = 0; Set result = new LinkedHashSet<>(); + Set neighborhoodItems = new LinkedHashSet<>(); for (int[] src : sourcePositions) { LOGGER.info("[FurniNeighborhood] Source: ({},{}), offsets: {}", src[0], src[1], tileOffsets.size()); + for (int[] offset : getFullGridOffsets()) { + int tx = src[0] + (offset[0] - this.targetOffsetX); + int ty = src[1] + (offset[1] - this.targetOffsetY); + + for (HabboItem item : room.getItemsAt(tx, ty)) { + if (item != null && (includeWiredItems || !(item instanceof InteractionWired))) { + neighborhoodItems.add(item); + } + } + } + for (int[] offset : tileOffsets) { int tx = src[0] + (offset[0] - this.targetOffsetX); int ty = src[1] + (offset[1] - this.targetOffsetY); @@ -91,7 +104,7 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { } LOGGER.info("[FurniNeighborhood] Raw={}, wiredSkipped={}, kept={}", totalRaw, wiredSkipped, result.size()); - result = this.applySelectorModifiers(result, this.getSelectableFloorItems(room, ctx), ctx.targets().items(), filterExisting, invert); + result = this.applyNeighborhoodModifiers(result, neighborhoodItems, ctx.targets().items()); // Always set the selector result — even if empty. // An empty result means no items matched the neighborhood, so downstream @@ -100,15 +113,51 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { LOGGER.info("[FurniNeighborhood] Set {} items as targets", result.size()); } + private List getFullGridOffsets() { + List offsets = new ArrayList<>(); + + for (int y = -GRID_RANGE; y <= GRID_RANGE; y++) { + for (int x = -GRID_RANGE; x <= GRID_RANGE; x++) { + offsets.add(new int[]{ x, y }); + } + } + + return offsets; + } + + private LinkedHashSet applyNeighborhoodModifiers(Set matchedTargets, + Set neighborhoodTargets, + Collection existingTargets) { + LinkedHashSet matched = new LinkedHashSet<>(matchedTargets); + + if (this.invert) { + LinkedHashSet base = new LinkedHashSet<>(neighborhoodTargets); + base.removeAll(matched); + + if (this.filterExisting) { + base.retainAll(this.toLinkedHashSet(existingTargets)); + } + + return base; + } + + if (this.filterExisting) { + matched.retainAll(this.toLinkedHashSet(existingTargets)); + } + + return matched; + } + private List resolveSourcePositions(WiredContext ctx, Room room) { switch (sourceType) { case SOURCE_USER_TRIGGER: { - if (ctx.tile().isPresent()) { - return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y }); + Optional actor = ctx.actor(); + if (actor.isPresent()) { + return Collections.singletonList(new int[]{ actor.get().getX(), actor.get().getY() }); } - return ctx.actor() - .map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() })) + return ctx.tile() + .map(tile -> Collections.singletonList(new int[]{ tile.x, tile.y })) .orElse(Collections.emptyList()); } case SOURCE_USER_SIGNAL: { @@ -260,6 +309,16 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { return true; } + @Override + public boolean hasRequiredSelectorTargets(WiredContext ctx) { + return ctx != null && ctx.targets().hasItems(); + } + + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson( diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniOnFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniOnFurni.java index 2b70e9b9..5ebc443b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniOnFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniOnFurni.java @@ -128,6 +128,11 @@ public class WiredEffectFurniOnFurni extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniPicks.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniPicks.java index 26c16426..df012d6a 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniPicks.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniPicks.java @@ -86,6 +86,11 @@ public class WiredEffectFurniPicks extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniSignal.java index 1ba14216..00f2c635 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniSignal.java @@ -77,6 +77,11 @@ public class WiredEffectFurniSignal extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersArea.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersArea.java index 0763cf9e..fe3e89db 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersArea.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersArea.java @@ -86,6 +86,11 @@ public class WiredEffectUsersArea extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByAction.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByAction.java index 3f88d99a..7f5edf79 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByAction.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByAction.java @@ -92,6 +92,11 @@ public class WiredEffectUsersByAction extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByName.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByName.java index a8dc2ef8..cd0b1b25 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByName.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByName.java @@ -90,6 +90,11 @@ public class WiredEffectUsersByName extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(this.namesText, this.filterExisting, this.invert, this.getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByType.java index b9280099..9b73aa37 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByType.java @@ -76,6 +76,11 @@ public class WiredEffectUsersByType extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(this.entityType, this.filterExisting, this.invert, this.getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersGroup.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersGroup.java index f74de64a..cc78d186 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersGroup.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersGroup.java @@ -90,6 +90,11 @@ public class WiredEffectUsersGroup extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(this.groupType, this.selectedGroupId, this.filterExisting, this.invert, this.getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersHandItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersHandItem.java index e34ef8e7..c40c921e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersHandItem.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersHandItem.java @@ -73,6 +73,11 @@ public class WiredEffectUsersHandItem extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(this.handItemId, this.filterExisting, this.invert, this.getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java index db19bea6..454d12d2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java @@ -38,6 +38,7 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { private static final int MAX_PICKED_FURNI = 20; private static final int MAX_TILE_OFFSETS = 64; + private static final int GRID_RANGE = 4; private int sourceType = SOURCE_USER_TRIGGER; private boolean filterExisting = false; @@ -87,11 +88,25 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { LOGGER.debug("[Neighborhood] Target tiles: {}", targetTiles); + Set neighborhoodTiles = new HashSet<>(); + for (int[] src : sourcePositions) { + for (int[] offset : getFullGridOffsets()) { + int tx = src[0] + (offset[0] - this.targetOffsetX); + int ty = src[1] + (offset[1] - this.targetOffsetY); + neighborhoodTiles.add(tx + "," + ty); + } + } + List result = new ArrayList<>(); + List neighborhoodUsers = new ArrayList<>(); for (RoomUnit unit : room.getRoomUnits()) { String pos = unit.getX() + "," + unit.getY(); boolean onTile = targetTiles.contains(pos); + if (neighborhoodTiles.contains(pos)) { + neighborhoodUsers.add(unit); + } + LOGGER.debug("[Neighborhood] Unit id={} type={} pos={} onTile={}", unit.getId(), unit.getRoomUnitType(), pos, onTile); if (onTile) { @@ -99,7 +114,7 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { } } - result = new ArrayList<>(this.applySelectorModifiers(result, room.getRoomUnits(), ctx.targets().users(), filterExisting, invert)); + result = new ArrayList<>(this.applyNeighborhoodModifiers(result, neighborhoodUsers, ctx.targets().users())); LOGGER.debug("[Neighborhood] Result: {} users selected", result.size()); @@ -110,15 +125,51 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { ctx.targets().setUsers(result); } + private List getFullGridOffsets() { + List offsets = new ArrayList<>(); + + for (int y = -GRID_RANGE; y <= GRID_RANGE; y++) { + for (int x = -GRID_RANGE; x <= GRID_RANGE; x++) { + offsets.add(new int[]{ x, y }); + } + } + + return offsets; + } + + private LinkedHashSet applyNeighborhoodModifiers(Collection matchedTargets, + Collection neighborhoodTargets, + Collection existingTargets) { + LinkedHashSet matched = new LinkedHashSet<>(matchedTargets); + + if (this.invert) { + LinkedHashSet base = new LinkedHashSet<>(neighborhoodTargets); + base.removeAll(matched); + + if (this.filterExisting) { + base.retainAll(this.toLinkedHashSet(existingTargets)); + } + + return base; + } + + if (this.filterExisting) { + matched.retainAll(this.toLinkedHashSet(existingTargets)); + } + + return matched; + } + private List resolveSourcePositions(WiredContext ctx, Room room) { switch (sourceType) { case SOURCE_USER_TRIGGER: { - if (ctx.tile().isPresent()) { - return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y }); + Optional actor = ctx.actor(); + if (actor.isPresent()) { + return Collections.singletonList(new int[]{ actor.get().getX(), actor.get().getY() }); } - return ctx.actor() - .map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() })) + return ctx.tile() + .map(tile -> Collections.singletonList(new int[]{ tile.x, tile.y })) .orElse(Collections.emptyList()); } case SOURCE_USER_SIGNAL: { @@ -262,6 +313,16 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { return true; } + @Override + public boolean hasRequiredSelectorTargets(WiredContext ctx) { + return ctx != null && ctx.targets().hasUsers(); + } + + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson( diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersOnFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersOnFurni.java index 149ae0cb..f0d350a6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersOnFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersOnFurni.java @@ -115,6 +115,11 @@ public class WiredEffectUsersOnFurni extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersSignal.java index 1ce2eaba..517f0116 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersSignal.java @@ -71,6 +71,11 @@ public class WiredEffectUsersSignal extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersTeam.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersTeam.java index 8b16f73f..7e03100e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersTeam.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersTeam.java @@ -76,6 +76,11 @@ public class WiredEffectUsersTeam extends InteractionWiredEffect { return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData(this.teamType, this.filterExisting, this.invert, this.getDelay())); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java index 11916849..b28ea944 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java @@ -187,6 +187,11 @@ public abstract class WiredEffectVariableSelectorBase extends InteractionWiredEf return true; } + @Override + public boolean usesExistingSelectorTargets() { + return this.filterExisting; + } + @Override public String getWiredData() { this.refreshReferenceItems(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java index 5367e136..3ab28e85 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/pets/PetManager.java @@ -370,8 +370,14 @@ public class PetManager { } else { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { LOGGER.error("Missing petdata for type {}. Adding this to the database...", type); - try (PreparedStatement statement = connection.prepareStatement("INSERT INTO pet_actions (pet_type) VALUES (?)")) { + try (PreparedStatement statement = connection.prepareStatement("INSERT INTO pet_actions (pet_type, pet_name, offspring_type, happy_actions, tired_actions, random_actions, can_swim) VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement.setInt(1, type); + statement.setString(2, getFallbackPetName(type)); + statement.setInt(3, getFallbackOffspringType(type)); + statement.setString(4, ""); + statement.setString(5, ""); + statement.setString(6, ""); + statement.setString(7, "0"); statement.execute(); } @@ -411,6 +417,42 @@ public class PetManager { return this.petData.values(); } + private static String getFallbackPetName(int type) { + switch (type) { + case 0: + return "Dog"; + case 1: + return "Cat"; + case 2: + return "Crocodile"; + case 3: + return "Terrier"; + case 4: + return "Bear"; + case 5: + return "Pig"; + default: + return "pet_type_" + type; + } + } + + private static int getFallbackOffspringType(int type) { + switch (type) { + case 0: + return 29; + case 1: + return 28; + case 3: + return 25; + case 4: + return 24; + case 5: + return 30; + default: + return -1; + } + } + public Pet createPet(Item item, String name, String race, String color, GameClient client) { int type = Integer.parseInt(item.getName().toLowerCase().replace("a0 pet", "")); @@ -540,4 +582,4 @@ public class PetManager { return false; } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java index 06089a0d..9028c0db 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java @@ -1177,7 +1177,7 @@ public class Room implements Comparable, ISerialize, Runnable { if (this.needsUpdate) { try (Connection connection = Emulator.getDatabase().getDataSource() .getConnection(); PreparedStatement statement = connection.prepareStatement( - "UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ?, youtube_enabled = ? WHERE id = ?")) { + "UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ?, youtube_enabled = ?, builders_club_trial_locked = ?, builders_club_original_state = ? WHERE id = ?")) { statement.setString(1, this.name); statement.setString(2, this.description); statement.setString(3, this.password); @@ -1228,7 +1228,9 @@ public class Room implements Comparable, ISerialize, Runnable { statement.setString(39, this.hideWired ? "1" : "0"); statement.setString(40, this.allowUnderpass ? "1" : "0"); statement.setString(41, this.youtubeEnabled ? "1" : "0"); - statement.setInt(42, this.id); + statement.setString(42, this.buildersClubTrialLocked ? "1" : "0"); + statement.setString(43, (this.buildersClubOriginalState != null ? this.buildersClubOriginalState : RoomState.OPEN).name().toLowerCase()); + statement.setInt(44, this.id); statement.executeUpdate(); this.needsUpdate = false; } catch (SQLException e) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessage.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessage.java index b6f83d43..6ddc1796 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessage.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessage.java @@ -4,6 +4,7 @@ import com.eu.habbo.Emulator; import com.eu.habbo.core.DatabaseLoggable; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserCustomizationData; import com.eu.habbo.messages.ISerialize; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.Incoming; @@ -204,23 +205,14 @@ public class RoomChatMessage implements Runnable, ISerialize, DatabaseLoggable { message.appendInt(this.getMessage().length()); // Custom prefix data - String prefixText = ""; - String prefixColor = ""; - String prefixIcon = ""; - String prefixEffect = ""; - if (this.habbo != null && this.habbo.getInventory() != null && this.habbo.getInventory().getPrefixesComponent() != null) { - com.eu.habbo.habbohotel.users.UserPrefix activePrefix = this.habbo.getInventory().getPrefixesComponent().getActivePrefix(); - if (activePrefix != null) { - prefixText = activePrefix.getText(); - prefixColor = activePrefix.getColor(); - prefixIcon = activePrefix.getIcon(); - prefixEffect = activePrefix.getEffect(); - } - } - message.appendString(prefixText); - message.appendString(prefixColor); - message.appendString(prefixIcon); - message.appendString(prefixEffect); + UserCustomizationData customizationData = (this.habbo != null) ? UserCustomizationData.fromHabbo(this.habbo) : UserCustomizationData.empty(); + message.appendString(customizationData.prefixText); + message.appendString(customizationData.prefixColor); + message.appendString(customizationData.prefixIcon); + message.appendString(customizationData.prefixEffect); + message.appendString(customizationData.prefixFont); + message.appendString(customizationData.nickIcon); + message.appendString(customizationData.displayOrder); } catch (Exception e) { LOGGER.error("Caught exception", e); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java index dcbdfd11..395e7a0f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java @@ -343,18 +343,16 @@ public class RoomSpecialTypes { * Adds a wired trigger to the room. * @param trigger The trigger to add */ - public static final int MAX_SIGNAL_SENDERS_PER_ROOM = 25; - public static final int MAX_SIGNAL_RECEIVERS_PER_ROOM = 5; - public static final int MAX_SENDERS_PER_RECEIVER = 5; + public static final int MAX_SIGNAL_SENDERS_PER_ROOM = 0; + public static final int MAX_SIGNAL_RECEIVERS_PER_ROOM = 0; + public static final int MAX_SENDERS_PER_RECEIVER = 0; public boolean isSignalSenderLimitReached() { - Set existing = this.getSignalSenders(); - return existing != null && existing.size() >= MAX_SIGNAL_SENDERS_PER_ROOM; + return false; } public boolean isSignalReceiverLimitReached() { - Set existing = this.wiredTriggers.get(WiredTriggerType.RECEIVE_SIGNAL); - return existing != null && existing.size() >= MAX_SIGNAL_RECEIVERS_PER_ROOM; + return false; } public int countSendersTargetingReceiver(int receiverItemId, InteractionWiredEffect excludeSender) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/translations/GoogleTranslateManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/translations/GoogleTranslateManager.java new file mode 100644 index 00000000..3e60dc90 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/translations/GoogleTranslateManager.java @@ -0,0 +1,469 @@ +package com.eu.habbo.habbohotel.translations; + +import com.eu.habbo.Emulator; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.HttpsURLConnection; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class GoogleTranslateManager { + private static final Logger LOGGER = LoggerFactory.getLogger(GoogleTranslateManager.class); + private static final int DEFAULT_TIMEOUT_MS = 5000; + private static final long CACHE_TTL_MS = 1000L * 60L * 60L * 6L; + private static final int MAX_TRANSLATION_CACHE_SIZE = 2048; + private static final int MAX_LANGUAGE_CACHE_SIZE = 32; + private static final String FREE_TRANSLATE_ENDPOINT = "https://translate.googleapis.com/translate_a/single"; + private static final List FREE_SUPPORTED_LANGUAGES = buildFreeSupportedLanguages(); + + private final Map translationCache = Collections.synchronizedMap( + new LinkedHashMap(128, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return this.size() > MAX_TRANSLATION_CACHE_SIZE; + } + }); + private final Map languagesCache = Collections.synchronizedMap( + new LinkedHashMap(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return this.size() > MAX_LANGUAGE_CACHE_SIZE; + } + }); + + public SupportedLanguagesResponse getSupportedLanguages(String displayLanguage) { + String normalizedDisplayLanguage = normalizeLanguageCode(displayLanguage, "en"); + + CachedLanguages cachedLanguages = this.languagesCache.get(normalizedDisplayLanguage); + + if ((cachedLanguages != null) && !cachedLanguages.isExpired()) { + return SupportedLanguagesResponse.success(new ArrayList<>(cachedLanguages.languages)); + } + + ArrayList supportedLanguages = new ArrayList<>(FREE_SUPPORTED_LANGUAGES); + this.languagesCache.put(normalizedDisplayLanguage, new CachedLanguages(supportedLanguages)); + return SupportedLanguagesResponse.success(supportedLanguages); + } + + public TranslationResponse translate(String text, String targetLanguage) { + String safeText = text == null ? "" : text; + String normalizedTargetLanguage = normalizeLanguageCode(targetLanguage, "en"); + + if (safeText.trim().isEmpty()) { + return TranslationResponse.success(safeText, safeText, "", normalizedTargetLanguage); + } + + String cacheKey = normalizedTargetLanguage + '\u0000' + safeText; + CachedTranslation cachedTranslation = this.translationCache.get(cacheKey); + + if ((cachedTranslation != null) && !cachedTranslation.isExpired()) { + return cachedTranslation.response; + } + + try { + String requestUrl = FREE_TRANSLATE_ENDPOINT + + "?client=gtx" + + "&sl=auto" + + "&tl=" + encode(normalizedTargetLanguage) + + "&dt=t" + + "&q=" + encode(safeText); + HttpsURLConnection connection = this.openGet(requestUrl); + + int statusCode = connection.getResponseCode(); + + if (statusCode != 200) { + return TranslationResponse.failure(safeText, normalizedTargetLanguage, this.readErrorMessage(connection)); + } + + JsonArray response = this.readJsonArray(connection.getInputStream()); + JsonArray translatedParts = response.size() > 0 && response.get(0).isJsonArray() + ? response.get(0).getAsJsonArray() + : new JsonArray(); + StringBuilder translatedText = new StringBuilder(); + + for (int index = 0; index < translatedParts.size(); index++) { + if (!translatedParts.get(index).isJsonArray()) { + continue; + } + + JsonArray translatedPart = translatedParts.get(index).getAsJsonArray(); + + if (translatedPart.size() > 0 && !translatedPart.get(0).isJsonNull()) { + translatedText.append(translatedPart.get(0).getAsString()); + } + } + + String detectedLanguage = ""; + if (response.size() > 2 && !response.get(2).isJsonNull()) { + detectedLanguage = response.get(2).getAsString(); + } + + String resolvedTranslation = translatedText.length() > 0 ? translatedText.toString() : safeText; + TranslationResponse translationResponse = TranslationResponse.success(safeText, resolvedTranslation, detectedLanguage, normalizedTargetLanguage); + + this.translationCache.put(cacheKey, new CachedTranslation(translationResponse)); + + return translationResponse; + } catch (Exception e) { + LOGGER.error("Failed to translate text with Google Translate", e); + return TranslationResponse.failure(safeText, normalizedTargetLanguage, "Failed to translate text with Google Translate."); + } + } + + public void clearCache() { + this.translationCache.clear(); + this.languagesCache.clear(); + } + + private int getTimeoutMs() { + return Math.max(1000, Emulator.getConfig().getInt("translate.google.timeout.ms", DEFAULT_TIMEOUT_MS)); + } + + private HttpsURLConnection openGet(String requestUrl) throws IOException { + HttpsURLConnection connection = (HttpsURLConnection) URI.create(requestUrl).toURL().openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(this.getTimeoutMs()); + connection.setReadTimeout(this.getTimeoutMs()); + connection.setRequestProperty("Accept", "application/json"); + return connection; + } + + private JsonObject readJson(InputStream inputStream) throws IOException { + try (InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + return JsonParser.parseReader(bufferedReader).getAsJsonObject(); + } + } + + private JsonArray readJsonArray(InputStream inputStream) throws IOException { + try (InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + return JsonParser.parseReader(bufferedReader).getAsJsonArray(); + } + } + + private String readErrorMessage(HttpsURLConnection connection) { + try { + InputStream errorStream = connection.getErrorStream(); + + if (errorStream == null) { + return "Google Translate request failed with HTTP " + connection.getResponseCode() + '.'; + } + + try { + JsonObject errorResponse = this.readJson(errorStream); + + if (errorResponse.has("error") && errorResponse.get("error").isJsonObject()) { + JsonObject errorObject = errorResponse.getAsJsonObject("error"); + + if (errorObject.has("message")) { + return errorObject.get("message").getAsString(); + } + } + } catch (Exception ignored) { + try (InputStreamReader inputStreamReader = new InputStreamReader(errorStream, StandardCharsets.UTF_8); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + StringBuilder responseText = new StringBuilder(); + String line; + + while ((line = bufferedReader.readLine()) != null) { + responseText.append(line); + } + + if (responseText.length() > 0) { + return responseText.toString(); + } + } + } + } catch (Exception e) { + LOGGER.warn("Failed to parse Google Translate error response", e); + } + + try { + return "Google Translate request failed with HTTP " + connection.getResponseCode() + '.'; + } catch (IOException e) { + return "Google Translate request failed."; + } + } + + private static String normalizeLanguageCode(String languageCode, String fallback) { + if (languageCode == null || languageCode.trim().isEmpty()) { + return fallback; + } + + String normalized = languageCode.trim().replace('_', '-'); + String[] split = normalized.split("-"); + + if (split.length <= 1) { + return normalized; + } + + return split[0] + '-' + split[1].toUpperCase(); + } + + private static String encode(String value) { + return URLEncoder.encode(value == null ? "" : value, StandardCharsets.UTF_8); + } + + private static List buildFreeSupportedLanguages() { + ArrayList languages = new ArrayList<>(); + addLanguage(languages, "af", "Afrikaans"); + addLanguage(languages, "sq", "Albanian"); + addLanguage(languages, "am", "Amharic"); + addLanguage(languages, "ar", "Arabic"); + addLanguage(languages, "hy", "Armenian"); + addLanguage(languages, "az", "Azerbaijani"); + addLanguage(languages, "eu", "Basque"); + addLanguage(languages, "be", "Belarusian"); + addLanguage(languages, "bn", "Bengali"); + addLanguage(languages, "bs", "Bosnian"); + addLanguage(languages, "bg", "Bulgarian"); + addLanguage(languages, "ca", "Catalan"); + addLanguage(languages, "ceb", "Cebuano"); + addLanguage(languages, "ny", "Chichewa"); + addLanguage(languages, "zh-CN", "Chinese (Simplified)"); + addLanguage(languages, "zh-TW", "Chinese (Traditional)"); + addLanguage(languages, "co", "Corsican"); + addLanguage(languages, "hr", "Croatian"); + addLanguage(languages, "cs", "Czech"); + addLanguage(languages, "da", "Danish"); + addLanguage(languages, "nl", "Dutch"); + addLanguage(languages, "en", "English"); + addLanguage(languages, "eo", "Esperanto"); + addLanguage(languages, "et", "Estonian"); + addLanguage(languages, "tl", "Filipino"); + addLanguage(languages, "fi", "Finnish"); + addLanguage(languages, "fr", "French"); + addLanguage(languages, "fy", "Frisian"); + addLanguage(languages, "gl", "Galician"); + addLanguage(languages, "ka", "Georgian"); + addLanguage(languages, "de", "German"); + addLanguage(languages, "el", "Greek"); + addLanguage(languages, "gu", "Gujarati"); + addLanguage(languages, "ht", "Haitian Creole"); + addLanguage(languages, "ha", "Hausa"); + addLanguage(languages, "haw", "Hawaiian"); + addLanguage(languages, "iw", "Hebrew"); + addLanguage(languages, "hi", "Hindi"); + addLanguage(languages, "hmn", "Hmong"); + addLanguage(languages, "hu", "Hungarian"); + addLanguage(languages, "is", "Icelandic"); + addLanguage(languages, "ig", "Igbo"); + addLanguage(languages, "id", "Indonesian"); + addLanguage(languages, "ga", "Irish"); + addLanguage(languages, "it", "Italian"); + addLanguage(languages, "ja", "Japanese"); + addLanguage(languages, "jw", "Javanese"); + addLanguage(languages, "kn", "Kannada"); + addLanguage(languages, "kk", "Kazakh"); + addLanguage(languages, "km", "Khmer"); + addLanguage(languages, "rw", "Kinyarwanda"); + addLanguage(languages, "ko", "Korean"); + addLanguage(languages, "ku", "Kurdish"); + addLanguage(languages, "ky", "Kyrgyz"); + addLanguage(languages, "lo", "Lao"); + addLanguage(languages, "la", "Latin"); + addLanguage(languages, "lv", "Latvian"); + addLanguage(languages, "lt", "Lithuanian"); + addLanguage(languages, "lb", "Luxembourgish"); + addLanguage(languages, "mk", "Macedonian"); + addLanguage(languages, "mg", "Malagasy"); + addLanguage(languages, "ms", "Malay"); + addLanguage(languages, "ml", "Malayalam"); + addLanguage(languages, "mt", "Maltese"); + addLanguage(languages, "mi", "Maori"); + addLanguage(languages, "mr", "Marathi"); + addLanguage(languages, "mn", "Mongolian"); + addLanguage(languages, "my", "Myanmar"); + addLanguage(languages, "ne", "Nepali"); + addLanguage(languages, "no", "Norwegian"); + addLanguage(languages, "or", "Odia"); + addLanguage(languages, "ps", "Pashto"); + addLanguage(languages, "fa", "Persian"); + addLanguage(languages, "pl", "Polish"); + addLanguage(languages, "pt", "Portuguese"); + addLanguage(languages, "pa", "Punjabi"); + addLanguage(languages, "ro", "Romanian"); + addLanguage(languages, "ru", "Russian"); + addLanguage(languages, "sm", "Samoan"); + addLanguage(languages, "gd", "Scots"); + addLanguage(languages, "sr", "Serbian"); + addLanguage(languages, "st", "Sesotho"); + addLanguage(languages, "sn", "Shona"); + addLanguage(languages, "sd", "Sindhi"); + addLanguage(languages, "si", "Sinhala"); + addLanguage(languages, "sk", "Slovak"); + addLanguage(languages, "sl", "Slovenian"); + addLanguage(languages, "so", "Somali"); + addLanguage(languages, "es", "Spanish"); + addLanguage(languages, "su", "Sundanese"); + addLanguage(languages, "sw", "Swahili"); + addLanguage(languages, "sv", "Swedish"); + addLanguage(languages, "tg", "Tajik"); + addLanguage(languages, "ta", "Tamil"); + addLanguage(languages, "tt", "Tatar"); + addLanguage(languages, "te", "Telugu"); + addLanguage(languages, "th", "Thai"); + addLanguage(languages, "tr", "Turkish"); + addLanguage(languages, "tk", "Turkmen"); + addLanguage(languages, "uk", "Ukrainian"); + addLanguage(languages, "ur", "Urdu"); + addLanguage(languages, "ug", "Uyghur"); + addLanguage(languages, "uz", "Uzbek"); + addLanguage(languages, "vi", "Vietnamese"); + addLanguage(languages, "cy", "Welsh"); + addLanguage(languages, "xh", "Xhosa"); + addLanguage(languages, "yi", "Yiddish"); + addLanguage(languages, "yo", "Yoruba"); + addLanguage(languages, "zu", "Zulu"); + languages.sort(Comparator.comparing(SupportedLanguage::getName, String.CASE_INSENSITIVE_ORDER)); + return Collections.unmodifiableList(languages); + } + + private static void addLanguage(List languages, String code, String name) { + languages.add(new SupportedLanguage(code, name)); + } + + public static class SupportedLanguage { + private final String code; + private final String name; + + public SupportedLanguage(String code, String name) { + this.code = code; + this.name = name; + } + + public String getCode() { + return this.code; + } + + public String getName() { + return this.name; + } + } + + public static class SupportedLanguagesResponse { + private final boolean success; + private final String errorMessage; + private final List languages; + + private SupportedLanguagesResponse(boolean success, String errorMessage, List languages) { + this.success = success; + this.errorMessage = errorMessage == null ? "" : errorMessage; + this.languages = languages == null ? Collections.emptyList() : languages; + } + + public static SupportedLanguagesResponse success(List languages) { + return new SupportedLanguagesResponse(true, "", languages); + } + + public static SupportedLanguagesResponse failure(String errorMessage) { + return new SupportedLanguagesResponse(false, errorMessage, Collections.emptyList()); + } + + public boolean isSuccess() { + return this.success; + } + + public String getErrorMessage() { + return this.errorMessage; + } + + public List getLanguages() { + return this.languages; + } + } + + public static class TranslationResponse { + private final boolean success; + private final String errorMessage; + private final String originalText; + private final String translatedText; + private final String detectedLanguage; + private final String targetLanguage; + + private TranslationResponse(boolean success, String errorMessage, String originalText, String translatedText, String detectedLanguage, String targetLanguage) { + this.success = success; + this.errorMessage = errorMessage == null ? "" : errorMessage; + this.originalText = originalText == null ? "" : originalText; + this.translatedText = translatedText == null ? "" : translatedText; + this.detectedLanguage = detectedLanguage == null ? "" : detectedLanguage; + this.targetLanguage = targetLanguage == null ? "" : targetLanguage; + } + + public static TranslationResponse success(String originalText, String translatedText, String detectedLanguage, String targetLanguage) { + return new TranslationResponse(true, "", originalText, translatedText, detectedLanguage, targetLanguage); + } + + public static TranslationResponse failure(String originalText, String targetLanguage, String errorMessage) { + return new TranslationResponse(false, errorMessage, originalText, originalText, "", targetLanguage); + } + + public boolean isSuccess() { + return this.success; + } + + public String getErrorMessage() { + return this.errorMessage; + } + + public String getOriginalText() { + return this.originalText; + } + + public String getTranslatedText() { + return this.translatedText; + } + + public String getDetectedLanguage() { + return this.detectedLanguage; + } + + public String getTargetLanguage() { + return this.targetLanguage; + } + } + + private static class CachedTranslation { + private final long createdAt; + private final TranslationResponse response; + + private CachedTranslation(TranslationResponse response) { + this.createdAt = System.currentTimeMillis(); + this.response = response; + } + + private boolean isExpired() { + return (System.currentTimeMillis() - this.createdAt) > CACHE_TTL_MS; + } + } + + private static class CachedLanguages { + private final long createdAt; + private final List languages; + + private CachedLanguages(List languages) { + this.createdAt = System.currentTimeMillis(); + this.languages = languages; + } + + private boolean isExpired() { + return (System.currentTimeMillis() - this.createdAt) > CACHE_TTL_MS; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInventory.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInventory.java index 6fdba07d..5e0130ff 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInventory.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInventory.java @@ -23,6 +23,8 @@ public class HabboInventory { private ItemsComponent itemsComponent; private PetsComponent petsComponent; private PrefixesComponent prefixesComponent; + private NickIconsComponent nickIconsComponent; + private UserVisualSettingsComponent userVisualSettingsComponent; public HabboInventory(Habbo habbo) { this.habbo = habbo; @@ -68,6 +70,18 @@ public class HabboInventory { LOGGER.error("Caught exception", e); } + try { + this.nickIconsComponent = new NickIconsComponent(this.habbo); + } catch (Exception e) { + LOGGER.error("Caught exception", e); + } + + try { + this.userVisualSettingsComponent = new UserVisualSettingsComponent(this.habbo); + } catch (Exception e) { + LOGGER.error("Caught exception", e); + } + this.items = MarketPlace.getOwnOffers(this.habbo); } @@ -127,6 +141,22 @@ public class HabboInventory { this.prefixesComponent = prefixesComponent; } + public NickIconsComponent getNickIconsComponent() { + return this.nickIconsComponent; + } + + public void setNickIconsComponent(NickIconsComponent nickIconsComponent) { + this.nickIconsComponent = nickIconsComponent; + } + + public UserVisualSettingsComponent getUserVisualSettingsComponent() { + return this.userVisualSettingsComponent; + } + + public void setUserVisualSettingsComponent(UserVisualSettingsComponent userVisualSettingsComponent) { + this.userVisualSettingsComponent = userVisualSettingsComponent; + } + public void dispose() { this.badgesComponent.dispose(); this.botsComponent.dispose(); @@ -135,6 +165,8 @@ public class HabboInventory { this.petsComponent.dispose(); this.wardrobeComponent.dispose(); this.prefixesComponent.dispose(); + this.nickIconsComponent.dispose(); + this.userVisualSettingsComponent.dispose(); this.badgesComponent = null; this.botsComponent = null; @@ -143,6 +175,8 @@ public class HabboInventory { this.petsComponent = null; this.wardrobeComponent = null; this.prefixesComponent = null; + this.nickIconsComponent = null; + this.userVisualSettingsComponent = null; } public void addMarketplaceOffer(MarketPlaceOffer marketPlaceOffer) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserCustomizationData.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserCustomizationData.java new file mode 100644 index 00000000..371045b0 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserCustomizationData.java @@ -0,0 +1,121 @@ +package com.eu.habbo.habbohotel.users; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.inventory.UserVisualSettingsComponent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class UserCustomizationData { + private static final Logger LOGGER = LoggerFactory.getLogger(UserCustomizationData.class); + + public final String nickIcon; + public final String displayOrder; + public final String prefixText; + public final String prefixColor; + public final String prefixIcon; + public final String prefixEffect; + public final String prefixFont; + + private UserCustomizationData(String nickIcon, String displayOrder, String prefixText, String prefixColor, String prefixIcon, String prefixEffect, String prefixFont) { + this.nickIcon = nickIcon != null ? nickIcon : ""; + this.displayOrder = UserVisualSettingsComponent.sanitizeDisplayOrder(displayOrder); + this.prefixText = prefixText != null ? prefixText : ""; + this.prefixColor = prefixColor != null ? prefixColor : ""; + this.prefixIcon = prefixIcon != null ? prefixIcon : ""; + this.prefixEffect = prefixEffect != null ? prefixEffect : ""; + this.prefixFont = prefixFont != null ? prefixFont : ""; + } + + public static UserCustomizationData fromHabbo(Habbo habbo) { + if (habbo == null) { + return empty(); + } + + String nickIcon = ""; + String displayOrder = UserVisualSettingsComponent.DEFAULT_DISPLAY_ORDER; + String prefixText = ""; + String prefixColor = ""; + String prefixIcon = ""; + String prefixEffect = ""; + String prefixFont = ""; + + if (habbo.getInventory() != null) { + if (habbo.getInventory().getNickIconsComponent() != null) { + UserNickIcon activeNickIcon = habbo.getInventory().getNickIconsComponent().getActiveNickIcon(); + + if (activeNickIcon != null && activeNickIcon.getIconKey() != null) { + nickIcon = activeNickIcon.getIconKey(); + } + } + + if (habbo.getInventory().getPrefixesComponent() != null) { + UserPrefix activePrefix = habbo.getInventory().getPrefixesComponent().getActivePrefix(); + + if (activePrefix != null) { + prefixText = activePrefix.getText(); + prefixColor = activePrefix.getColor(); + prefixIcon = activePrefix.getIcon(); + prefixEffect = activePrefix.getEffect(); + prefixFont = activePrefix.getFont(); + } + } + + if (habbo.getInventory().getUserVisualSettingsComponent() != null) { + displayOrder = habbo.getInventory().getUserVisualSettingsComponent().getDisplayOrder(); + } + } + + return new UserCustomizationData(nickIcon, displayOrder, prefixText, prefixColor, prefixIcon, prefixEffect, prefixFont); + } + + public static UserCustomizationData fromUserId(int userId) { + String nickIcon = ""; + String prefixText = ""; + String prefixColor = ""; + String prefixIcon = ""; + String prefixEffect = ""; + String prefixFont = ""; + String displayOrder = UserVisualSettingsComponent.loadDisplayOrder(userId); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + try (PreparedStatement nickStatement = connection.prepareStatement( + "SELECT icon_key FROM user_nick_icons WHERE user_id = ? AND active = 1 LIMIT 1")) { + nickStatement.setInt(1, userId); + + try (ResultSet set = nickStatement.executeQuery()) { + if (set.next()) { + nickIcon = set.getString("icon_key"); + } + } + } + + try (PreparedStatement prefixStatement = connection.prepareStatement( + "SELECT text, color, icon, effect, font FROM user_prefixes WHERE user_id = ? AND active = 1 LIMIT 1")) { + prefixStatement.setInt(1, userId); + + try (ResultSet set = prefixStatement.executeQuery()) { + if (set.next()) { + prefixText = set.getString("text"); + prefixColor = set.getString("color"); + prefixIcon = set.getString("icon"); + prefixEffect = set.getString("effect"); + prefixFont = set.getString("font"); + } + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception while loading user customization data", e); + } + + return new UserCustomizationData(nickIcon, displayOrder, prefixText, prefixColor, prefixIcon, prefixEffect, prefixFont); + } + + public static UserCustomizationData empty() { + return new UserCustomizationData("", UserVisualSettingsComponent.DEFAULT_DISPLAY_ORDER, "", "", "", "", ""); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserNickIcon.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserNickIcon.java new file mode 100644 index 00000000..9b634f18 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserNickIcon.java @@ -0,0 +1,118 @@ +package com.eu.habbo.habbohotel.users; + +import com.eu.habbo.Emulator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.*; + +public class UserNickIcon implements Runnable { + private static final Logger LOGGER = LoggerFactory.getLogger(UserNickIcon.class); + + private int id; + private final int userId; + private String iconKey; + private boolean active; + private boolean needsInsert; + private boolean needsUpdate; + private boolean needsDelete; + + public UserNickIcon(ResultSet set) throws SQLException { + this.id = set.getInt("id"); + this.userId = set.getInt("user_id"); + this.iconKey = set.getString("icon_key"); + this.active = set.getBoolean("active"); + this.needsInsert = false; + this.needsUpdate = false; + this.needsDelete = false; + } + + public UserNickIcon(int userId, String iconKey) { + this.id = 0; + this.userId = userId; + this.iconKey = iconKey; + this.active = false; + this.needsInsert = true; + this.needsUpdate = false; + this.needsDelete = false; + } + + @Override + public void run() { + try { + if (this.needsInsert) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO user_nick_icons (user_id, icon_key, active) VALUES (?, ?, ?)", + Statement.RETURN_GENERATED_KEYS)) { + statement.setInt(1, this.userId); + statement.setString(2, this.iconKey); + statement.setBoolean(3, this.active); + statement.execute(); + + try (ResultSet set = statement.getGeneratedKeys()) { + if (set.next()) { + this.id = set.getInt(1); + } + } + } + + this.needsInsert = false; + } else if (this.needsDelete) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "DELETE FROM user_nick_icons WHERE id = ? AND user_id = ?")) { + statement.setInt(1, this.id); + statement.setInt(2, this.userId); + statement.execute(); + } + + this.needsDelete = false; + } else if (this.needsUpdate) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "UPDATE user_nick_icons SET icon_key = ?, active = ? WHERE id = ? AND user_id = ?")) { + statement.setString(1, this.iconKey); + statement.setBoolean(2, this.active); + statement.setInt(3, this.id); + statement.setInt(4, this.userId); + statement.execute(); + } + + this.needsUpdate = false; + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + } + + public int getId() { + return this.id; + } + + public int getUserId() { + return this.userId; + } + + public String getIconKey() { + return this.iconKey; + } + + public void setIconKey(String iconKey) { + this.iconKey = iconKey; + this.needsUpdate = true; + } + + public boolean isActive() { + return this.active; + } + + public void setActive(boolean active) { + this.active = active; + this.needsUpdate = true; + } + + public void needsDelete(boolean needsDelete) { + this.needsDelete = needsDelete; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserPrefix.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserPrefix.java index 6879d1be..b1b60b50 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserPrefix.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserPrefix.java @@ -15,6 +15,12 @@ public class UserPrefix implements Runnable { private String color; private String icon; private String effect; + private String font; + private int catalogPrefixId; + private String displayName; + private int points; + private int pointsType; + private boolean custom; private boolean active; private boolean needsInsert; private boolean needsUpdate; @@ -29,6 +35,12 @@ public class UserPrefix implements Runnable { if (this.icon == null) this.icon = ""; this.effect = set.getString("effect"); if (this.effect == null) this.effect = ""; + this.font = readString(set, "font", ""); + this.catalogPrefixId = readInt(set, "catalog_prefix_id", 0); + this.displayName = readString(set, "display_name", this.text); + this.points = readInt(set, "points", 0); + this.pointsType = readInt(set, "points_type", 0); + this.custom = readBoolean(set, "is_custom", true); this.active = set.getBoolean("active"); this.needsInsert = false; this.needsUpdate = false; @@ -36,12 +48,22 @@ public class UserPrefix implements Runnable { } public UserPrefix(int userId, String text, String color, String icon, String effect) { + this(userId, text, color, icon, effect, "", 0, text, 0, 0, true); + } + + public UserPrefix(int userId, String text, String color, String icon, String effect, String font, int catalogPrefixId, String displayName, int points, int pointsType, boolean custom) { this.id = 0; this.userId = userId; this.text = text; this.color = color; this.icon = icon != null ? icon : ""; this.effect = effect != null ? effect : ""; + this.font = font != null ? font : ""; + this.catalogPrefixId = catalogPrefixId; + this.displayName = (displayName != null && !displayName.isEmpty()) ? displayName : text; + this.points = points; + this.pointsType = pointsType; + this.custom = custom; this.active = false; this.needsInsert = true; this.needsUpdate = false; @@ -54,14 +76,20 @@ public class UserPrefix implements Runnable { if (this.needsInsert) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "INSERT INTO user_prefixes (user_id, text, color, icon, effect, active) VALUES (?, ?, ?, ?, ?, ?)", + "INSERT INTO user_prefixes (user_id, text, color, icon, effect, font, active, catalog_prefix_id, display_name, points, points_type, is_custom) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { statement.setInt(1, this.userId); statement.setString(2, this.text); statement.setString(3, this.color); statement.setString(4, this.icon); statement.setString(5, this.effect); - statement.setBoolean(6, this.active); + statement.setString(6, this.font); + statement.setBoolean(7, this.active); + statement.setInt(8, this.catalogPrefixId); + statement.setString(9, this.displayName); + statement.setInt(10, this.points); + statement.setInt(11, this.pointsType); + statement.setBoolean(12, this.custom); statement.execute(); try (ResultSet set = statement.getGeneratedKeys()) { if (set.next()) { @@ -82,14 +110,20 @@ public class UserPrefix implements Runnable { } else if (this.needsUpdate) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "UPDATE user_prefixes SET text = ?, color = ?, icon = ?, effect = ?, active = ? WHERE id = ? AND user_id = ?")) { + "UPDATE user_prefixes SET text = ?, color = ?, icon = ?, effect = ?, font = ?, active = ?, catalog_prefix_id = ?, display_name = ?, points = ?, points_type = ?, is_custom = ? WHERE id = ? AND user_id = ?")) { statement.setString(1, this.text); statement.setString(2, this.color); statement.setString(3, this.icon); statement.setString(4, this.effect); - statement.setBoolean(5, this.active); - statement.setInt(6, this.id); - statement.setInt(7, this.userId); + statement.setString(5, this.font); + statement.setBoolean(6, this.active); + statement.setInt(7, this.catalogPrefixId); + statement.setString(8, this.displayName); + statement.setInt(9, this.points); + statement.setInt(10, this.pointsType); + statement.setBoolean(11, this.custom); + statement.setInt(12, this.id); + statement.setInt(13, this.userId); statement.execute(); } this.needsUpdate = false; @@ -109,6 +143,13 @@ public class UserPrefix implements Runnable { public void setIcon(String icon) { this.icon = icon != null ? icon : ""; } public String getEffect() { return this.effect; } public void setEffect(String effect) { this.effect = effect != null ? effect : ""; } + public String getFont() { return this.font; } + public void setFont(String font) { this.font = font != null ? font : ""; } + public int getCatalogPrefixId() { return this.catalogPrefixId; } + public String getDisplayName() { return this.displayName; } + public int getPoints() { return this.points; } + public int getPointsType() { return this.pointsType; } + public boolean isCustom() { return this.custom; } public boolean isActive() { return this.active; } public void setActive(boolean active) { @@ -119,4 +160,29 @@ public class UserPrefix implements Runnable { public void needsUpdate(boolean needsUpdate) { this.needsUpdate = needsUpdate; } public void needsInsert(boolean needsInsert) { this.needsInsert = needsInsert; } public void needsDelete(boolean needsDelete) { this.needsDelete = needsDelete; } + + private static int readInt(ResultSet set, String columnName, int defaultValue) { + try { + return set.getInt(columnName); + } catch (SQLException e) { + return defaultValue; + } + } + + private static String readString(ResultSet set, String columnName, String defaultValue) { + try { + String value = set.getString(columnName); + return value != null ? value : defaultValue; + } catch (SQLException e) { + return defaultValue; + } + } + + private static boolean readBoolean(ResultSet set, String columnName, boolean defaultValue) { + try { + return set.getBoolean(columnName); + } catch (SQLException e) { + return defaultValue; + } + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/ItemsComponent.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/ItemsComponent.java index 8f496d17..5ada79ca 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/ItemsComponent.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/ItemsComponent.java @@ -1,7 +1,6 @@ package com.eu.habbo.habbohotel.users.inventory; import com.eu.habbo.Emulator; -import com.eu.habbo.database.SqlQueries; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboInventory; @@ -10,6 +9,7 @@ import com.eu.habbo.plugin.events.inventory.InventoryItemAddedEvent; import com.eu.habbo.plugin.events.inventory.InventoryItemRemovedEvent; import com.eu.habbo.plugin.events.inventory.InventoryItemsAddedEvent; import gnu.trove.TCollections; +import gnu.trove.iterator.TIntObjectIterator; import gnu.trove.map.TIntObjectMap; import gnu.trove.map.hash.THashMap; import gnu.trove.map.hash.TIntObjectHashMap; @@ -18,9 +18,11 @@ import gnu.trove.set.hash.THashSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; +import java.util.NoSuchElementException; public class ItemsComponent { private static final Logger LOGGER = LoggerFactory.getLogger(ItemsComponent.class); @@ -37,23 +39,25 @@ public class ItemsComponent { public static THashMap loadItems(Habbo habbo) { THashMap itemsList = new THashMap<>(); - try { - SqlQueries.forEach( - "SELECT * FROM items WHERE room_id = ? AND user_id = ?", - rs -> { - try { - HabboItem item = Emulator.getGameEnvironment().getItemManager().loadHabboItem(rs); - if (item != null) { - itemsList.put(rs.getInt("id"), item); - } else { - LOGGER.error("Failed to load HabboItem: {}", rs.getInt("id")); - } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT items.* FROM items LEFT JOIN builders_club_items ON builders_club_items.item_id = items.id WHERE items.room_id = ? AND items.user_id = ? AND builders_club_items.item_id IS NULL")) { + statement.setInt(1, 0); + statement.setInt(2, habbo.getHabboInfo().getId()); + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + try { + HabboItem item = Emulator.getGameEnvironment().getItemManager().loadHabboItem(set); + + if (item != null) { + itemsList.put(set.getInt("id"), item); + } else { + LOGGER.error("Failed to load HabboItem: {}", set.getInt("id")); } - }, - 0, habbo.getHabboInfo().getId()); - } catch (SqlQueries.DataAccessException e) { + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + } + } + } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); } @@ -147,45 +151,70 @@ public class ItemsComponent { public void dispose() { synchronized (this.items) { - if (!this.items.isEmpty()) { - List updates = new ArrayList<>(); - List deletes = new ArrayList<>(); - for (HabboItem item : this.items.valueCollection()) { - if (item.needsDelete()) { - deletes.add(item); - item.needsUpdate(false); - item.needsDelete(false); - } else if (item.needsUpdate()) { - updates.add(item); - item.needsUpdate(false); - } - } + TIntObjectIterator items = this.items.iterator(); - try { - if (!deletes.isEmpty()) { - SqlQueries.batchUpdate( - "DELETE FROM items WHERE id = ?", - deletes, - (ps, item) -> ps.setInt(1, item.getId())); + if (items == null) { + LOGGER.error("Items is NULL!"); + return; + } + + if (!this.items.isEmpty()) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + try (PreparedStatement updateStmt = connection.prepareStatement( + "UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?")) { + try (PreparedStatement deleteStmt = connection.prepareStatement( + "DELETE FROM items WHERE id = ?")) { + + int updateCount = 0; + int deleteCount = 0; + + for (int i = this.items.size(); i-- > 0; ) { + try { + items.advance(); + } catch (NoSuchElementException e) { + break; + } + + HabboItem item = items.value(); + if (item.needsDelete()) { + deleteStmt.setInt(1, item.getId()); + deleteStmt.addBatch(); + deleteCount++; + item.needsUpdate(false); + item.needsDelete(false); + } else if (item.needsUpdate()) { + updateStmt.setInt(1, item.getUserId()); + updateStmt.setInt(2, item.getRoomId()); + updateStmt.setString(3, item.getWallPosition()); + updateStmt.setInt(4, item.getX()); + updateStmt.setInt(5, item.getY()); + updateStmt.setDouble(6, item.getZ()); + updateStmt.setInt(7, item.getRotation()); + updateStmt.setString(8, item.getExtradata()); + updateStmt.setString(9, item.getLimitedStack() + ":" + item.getLimitedSells()); + updateStmt.setInt(10, item.getId()); + updateStmt.addBatch(); + updateCount++; + item.needsUpdate(false); + } + + if (updateCount > 0 && updateCount % 100 == 0) { + updateStmt.executeBatch(); + } + if (deleteCount > 0 && deleteCount % 100 == 0) { + deleteStmt.executeBatch(); + } + } + + if (deleteCount % 100 != 0) { + deleteStmt.executeBatch(); + } + if (updateCount % 100 != 0) { + updateStmt.executeBatch(); + } + } } - if (!updates.isEmpty()) { - SqlQueries.batchUpdate( - "UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?", - updates, - (ps, item) -> { - ps.setInt(1, item.getUserId()); - ps.setInt(2, item.getRoomId()); - ps.setString(3, item.getWallPosition()); - ps.setInt(4, item.getX()); - ps.setInt(5, item.getY()); - ps.setDouble(6, item.getZ()); - ps.setInt(7, item.getRotation()); - ps.setString(8, item.getExtradata()); - ps.setString(9, item.getLimitedStack() + ":" + item.getLimitedSells()); - ps.setInt(10, item.getId()); - }); - } - } catch (SqlQueries.DataAccessException e) { + } catch (SQLException e) { LOGGER.error("Caught SQL exception during batch item save", e); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/NickIconsComponent.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/NickIconsComponent.java new file mode 100644 index 00000000..b3ff37cb --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/NickIconsComponent.java @@ -0,0 +1,119 @@ +package com.eu.habbo.habbohotel.users.inventory; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserNickIcon; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class NickIconsComponent { + private static final Logger LOGGER = LoggerFactory.getLogger(NickIconsComponent.class); + + private final List nickIcons = new ArrayList<>(); + private final Habbo habbo; + + public NickIconsComponent(Habbo habbo) { + this.habbo = habbo; + this.loadNickIcons(); + } + + private void loadNickIcons() { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT * FROM user_nick_icons WHERE user_id = ?")) { + statement.setInt(1, this.habbo.getHabboInfo().getId()); + + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + this.nickIcons.add(new UserNickIcon(set)); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + } + + public List getNickIcons() { + synchronized (this.nickIcons) { + return new ArrayList<>(this.nickIcons); + } + } + + public UserNickIcon getActiveNickIcon() { + synchronized (this.nickIcons) { + for (UserNickIcon nickIcon : this.nickIcons) { + if (nickIcon.isActive()) { + return nickIcon; + } + } + } + + return null; + } + + public UserNickIcon getNickIcon(int id) { + synchronized (this.nickIcons) { + for (UserNickIcon nickIcon : this.nickIcons) { + if (nickIcon.getId() == id) { + return nickIcon; + } + } + } + + return null; + } + + public UserNickIcon getNickIconByKey(String iconKey) { + synchronized (this.nickIcons) { + for (UserNickIcon nickIcon : this.nickIcons) { + if (nickIcon.getIconKey().equalsIgnoreCase(iconKey)) { + return nickIcon; + } + } + } + + return null; + } + + public void addNickIcon(UserNickIcon nickIcon) { + synchronized (this.nickIcons) { + this.nickIcons.add(nickIcon); + } + } + + public void setActive(int nickIconId) { + synchronized (this.nickIcons) { + for (UserNickIcon nickIcon : this.nickIcons) { + boolean shouldBeActive = (nickIcon.getId() == nickIconId); + + if (nickIcon.isActive() != shouldBeActive) { + nickIcon.setActive(shouldBeActive); + Emulator.getThreading().run(nickIcon); + } + } + } + } + + public void deactivateAll() { + synchronized (this.nickIcons) { + for (UserNickIcon nickIcon : this.nickIcons) { + if (nickIcon.isActive()) { + nickIcon.setActive(false); + Emulator.getThreading().run(nickIcon); + } + } + } + } + + public void dispose() { + synchronized (this.nickIcons) { + this.nickIcons.clear(); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/PrefixesComponent.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/PrefixesComponent.java index 28889ede..e9563d72 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/PrefixesComponent.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/PrefixesComponent.java @@ -62,6 +62,15 @@ public class PrefixesComponent { return null; } + public UserPrefix getPrefixByCatalogId(int catalogPrefixId) { + synchronized (this.prefixes) { + for (UserPrefix prefix : this.prefixes) { + if (prefix.getCatalogPrefixId() == catalogPrefixId) return prefix; + } + } + return null; + } + public void addPrefix(UserPrefix prefix) { synchronized (this.prefixes) { this.prefixes.add(prefix); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/UserVisualSettingsComponent.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/UserVisualSettingsComponent.java new file mode 100644 index 00000000..6bb15c5e --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/UserVisualSettingsComponent.java @@ -0,0 +1,94 @@ +package com.eu.habbo.habbohotel.users.inventory; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class UserVisualSettingsComponent { + private static final Logger LOGGER = LoggerFactory.getLogger(UserVisualSettingsComponent.class); + public static final String DEFAULT_DISPLAY_ORDER = "icon-prefix-name"; + private static final Set ALLOWED_PARTS = new HashSet<>(Arrays.asList("icon", "prefix", "name")); + + private final Habbo habbo; + private String displayOrder = DEFAULT_DISPLAY_ORDER; + + public UserVisualSettingsComponent(Habbo habbo) { + this.habbo = habbo; + this.loadSettings(); + } + + private void loadSettings() { + this.displayOrder = loadDisplayOrder(this.habbo.getHabboInfo().getId()); + } + + public String getDisplayOrder() { + return sanitizeDisplayOrder(this.displayOrder); + } + + public void setDisplayOrder(String displayOrder) { + this.displayOrder = sanitizeDisplayOrder(displayOrder); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO user_visual_settings (user_id, display_order) VALUES (?, ?) ON DUPLICATE KEY UPDATE display_order = VALUES(display_order)")) { + statement.setInt(1, this.habbo.getHabboInfo().getId()); + statement.setString(2, this.displayOrder); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Caught SQL exception while saving user visual settings", e); + } + } + + public static String loadDisplayOrder(int userId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT display_order FROM user_visual_settings WHERE user_id = ? LIMIT 1")) { + statement.setInt(1, userId); + + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + return sanitizeDisplayOrder(set.getString("display_order")); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception while loading user visual settings", e); + } + + return DEFAULT_DISPLAY_ORDER; + } + + public static String sanitizeDisplayOrder(String displayOrder) { + if (displayOrder == null || displayOrder.trim().isEmpty()) { + return DEFAULT_DISPLAY_ORDER; + } + + String[] parts = displayOrder.trim().toLowerCase().split("-"); + + if (parts.length != 3) { + return DEFAULT_DISPLAY_ORDER; + } + + Set uniqueParts = new HashSet<>(); + + for (String part : parts) { + if (!ALLOWED_PARTS.contains(part) || !uniqueParts.add(part)) { + return DEFAULT_DISPLAY_ORDER; + } + } + + return String.join("-", parts); + } + + public void dispose() { + this.displayOrder = DEFAULT_DISPLAY_ORDER; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/IWiredEffect.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/IWiredEffect.java index ed972341..4e7cefb2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/IWiredEffect.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/IWiredEffect.java @@ -77,6 +77,22 @@ public interface IWiredEffect { return false; } + /** + * Selectors can use this to gate stack execution after their target list has + * been resolved. Returning false stops the stack before conditions/effects. + */ + default boolean hasRequiredSelectorTargets(WiredContext ctx) { + return true; + } + + /** + * Selectors that filter the current selector result should run after + * selectors that create/replace that result. + */ + default boolean usesExistingSelectorTargets() { + return false; + } + /** * Simulate this effect's execution and record intended state changes. *

diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java index 0faeee03..e88f2bf0 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java @@ -112,7 +112,7 @@ public final class WiredContext { this.state = state; this.legacySettings = legacySettings; this.contextVariables = (event.getContextVariableScope() != null) - ? event.getContextVariableScope() + ? event.getContextVariableScope().copy() : new WiredContextVariableScope(); this.targets = new WiredTargets(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java index 91f25afe..4301b2b8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java @@ -26,6 +26,7 @@ import com.eu.habbo.habbohotel.wired.api.WiredStack; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer; +import com.eu.habbo.messages.outgoing.rooms.items.ItemStateComposer; import com.eu.habbo.plugin.events.furniture.wired.WiredStackExecutedEvent; import com.eu.habbo.plugin.events.furniture.wired.WiredStackTriggeredEvent; import gnu.trove.map.hash.THashMap; @@ -130,6 +131,9 @@ public final class WiredEngine { /** Cache room+eventType+sourceItemId -> matching stacks for source-triggered timer events */ private final ConcurrentHashMap> sourceStacksByTriggerKey; + /** Track filter-selector animation tokens so rapid executions do not reset newer animations */ + private final ConcurrentHashMap filteredSelectorAnimationTokens; + /** * Create a new wired engine. * @@ -151,6 +155,7 @@ public final class WiredEngine { this.bannedRooms = new ConcurrentHashMap<>(); this.roomDiagnostics = new ConcurrentHashMap<>(); this.sourceStacksByTriggerKey = new ConcurrentHashMap<>(); + this.filteredSelectorAnimationTokens = new ConcurrentHashMap<>(); } /** @@ -426,6 +431,10 @@ public final class WiredEngine { applySelectionFilterExtras(stack, ctx, executedSelectors); } + if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) { + return false; + } + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); List executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event); @@ -541,6 +550,10 @@ public final class WiredEngine { applySelectionFilterExtras(stack, ctx, executedSelectors); } + if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) { + return false; + } + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); List executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event); @@ -627,6 +640,10 @@ public final class WiredEngine { applySelectionFilterExtras(stack, ctx, executedSelectors); } + if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) { + return false; + } + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); List executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); return !executableEffects.isEmpty(); @@ -660,6 +677,10 @@ public final class WiredEngine { applySelectionFilterExtras(stack, ctx, executedSelectors); } + if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) { + return false; + } + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, false); if (!conditionsPassedForExecution) { return false; @@ -1011,9 +1032,27 @@ public final class WiredEngine { if (effects.isEmpty()) return Collections.emptyList(); List executedSelectors = new ArrayList<>(); + List immediateSelectors = new ArrayList<>(); + List deferredSelectors = new ArrayList<>(); for (IWiredEffect effect : effects) { if (!effect.isSelector()) continue; + + if (effect.usesExistingSelectorTargets()) { + deferredSelectors.add(effect); + } else { + immediateSelectors.add(effect); + } + } + + executeSelectorList(immediateSelectors, ctx, executedSelectors); + executeSelectorList(deferredSelectors, ctx, executedSelectors); + + return executedSelectors; + } + + private void executeSelectorList(List selectors, WiredContext ctx, List executedSelectors) { + for (IWiredEffect effect : selectors) { if (effect.requiresActor() && !ctx.hasActor()) { continue; } @@ -1022,14 +1061,17 @@ public final class WiredEngine { try { effect.execute(ctx); if (effect instanceof InteractionWiredEffect) { - executedSelectors.add((InteractionWiredEffect) effect); + InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; + executedSelectors.add(wiredEffect); + + if (wiredEffect.usesExistingSelectorTargets()) { + setFilteredSelectorState(ctx.room(), wiredEffect, "3"); + } } } catch (Exception e) { LOGGER.warn("Error executing selector: {}", e.getMessage()); } } - - return executedSelectors; } private void finalizeSelectors(List executedSelectors, WiredContext ctx, long currentTime) { @@ -1042,7 +1084,56 @@ public final class WiredEngine { for (InteractionWiredEffect wiredEffect : executedSelectors) { wiredEffect.setCooldown(currentTime); - wiredEffect.activateBox(room, actor, currentTime); + + if (wiredEffect.usesExistingSelectorTargets()) { + animateFilteredSelectorBox(room, wiredEffect); + } else { + wiredEffect.activateBox(room, actor, currentTime); + } + } + } + + private void animateFilteredSelectorBox(Room room, InteractionWiredEffect wiredEffect) { + if (room == null || wiredEffect == null || room.isHideWired()) { + return; + } + + long animationToken = System.nanoTime(); + this.filteredSelectorAnimationTokens.put(wiredEffect.getId(), animationToken); + + setFilteredSelectorState(room, wiredEffect, "3", animationToken, false); + scheduleFilteredSelectorState(room, wiredEffect, "4", animationToken, 80L, false); + scheduleFilteredSelectorState(room, wiredEffect, "5", animationToken, 160L, false); + scheduleFilteredSelectorState(room, wiredEffect, "3", animationToken, 240L, true); + } + + private void scheduleFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state, long animationToken, long delay, boolean clearToken) { + Emulator.getThreading().run(() -> setFilteredSelectorState(room, wiredEffect, state, animationToken, clearToken), delay); + } + + private void setFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state) { + setFilteredSelectorState(room, wiredEffect, state, 0L, false); + } + + private void setFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state, long animationToken, boolean clearToken) { + if (room == null || wiredEffect == null || room.isHideWired()) { + return; + } + + if (animationToken != 0L) { + Long currentToken = this.filteredSelectorAnimationTokens.get(wiredEffect.getId()); + if (currentToken == null || currentToken != animationToken) { + return; + } + } + + if (!state.equals(wiredEffect.getExtradata())) { + wiredEffect.setExtradata(state); + room.sendComposer(new ItemStateComposer(wiredEffect).compose()); + } + + if (clearToken) { + this.filteredSelectorAnimationTokens.remove(wiredEffect.getId(), animationToken); } } @@ -1059,6 +1150,20 @@ public final class WiredEngine { WiredSelectionFilterSupport.applySelectorFilters(room, stack.triggerItem(), ctx); } + private boolean selectorsHaveRequiredTargets(List executedSelectors, WiredContext ctx) { + if (executedSelectors == null || executedSelectors.isEmpty()) { + return true; + } + + for (InteractionWiredEffect selector : executedSelectors) { + if (!selector.hasRequiredSelectorTargets(ctx)) { + return false; + } + } + + return true; + } + /** * Schedule a delayed effect execution. */ diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java index 00e41a15..201461e5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java @@ -151,8 +151,20 @@ public final class WiredSourceUtil { selectorCtx.setIncludeWiredSelectorItems(originalCtx.includeWiredSelectorItems()); List selectorEffects = getOrderedSelectorEffects(originalCtx, room, triggerItem); + executeSelectorEffects(selectorCtx, selectorEffects, false); + executeSelectorEffects(selectorCtx, selectorEffects, true); + applySelectionFilterExtras(room, triggerItem, selectorCtx); + + return selectorCtx; + } + + private static void executeSelectorEffects(WiredContext selectorCtx, List selectorEffects, boolean deferred) { for (InteractionWiredEffect effect : selectorEffects) { + if (effect == null || effect.usesExistingSelectorTargets() != deferred) { + continue; + } + if (effect.requiresActor() && !selectorCtx.hasActor()) { continue; } @@ -163,10 +175,6 @@ public final class WiredSourceUtil { } catch (Exception ignored) { } } - - applySelectionFilterExtras(room, triggerItem, selectorCtx); - - return selectorCtx; } private static WiredContext cloneSelectorContext(WiredContext originalCtx, boolean includeWiredItems) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java index cb73d427..1a6bc9d6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java @@ -64,8 +64,17 @@ public final class WiredTextInputCaptureSupport { return trigger.matches(stack.triggerItem(), event) ? CaptureResult.matched(new LinkedHashMap<>()) : CaptureResult.noMatch(); } - MatchResult matchResult = matchTemplate(trigger, text, capturersByName); + MatchResult matchResult = matchTemplate(trigger, text, capturersByName, room); if (!matchResult.matches) { + if (WiredManager.isDebugEnabled()) { + WiredManager.debug("[TextCapture] NO_MATCH room={} triggerId={} mode={} key='{}' text='{}' len={}", + room.getId(), + stack.triggerItem().getId(), + trigger.getMatchMode(), + safeForLog(trigger.getKey()), + safeForLog(text), + (text != null ? text.length() : 0)); + } return CaptureResult.noMatch(); } @@ -78,12 +87,28 @@ public final class WiredTextInputCaptureSupport { Integer resolvedValue = capturer.resolveCapturedValue(room, capture.getValue()); if (resolvedValue == null) { + if (WiredManager.isDebugEnabled()) { + WiredManager.debug("[TextCapture] RESOLVE_FAIL room={} triggerId={} capturer='{}' raw='{}' rawLen={}", + room.getId(), + stack.triggerItem().getId(), + capture.getKey(), + safeForLog(capture.getValue()), + (capture.getValue() != null ? capture.getValue().length() : 0)); + } return CaptureResult.noMatch(); } capturedValues.put(capturer.getVariableItemId(), resolvedValue); } + if (WiredManager.isDebugEnabled()) { + WiredManager.debug("[TextCapture] MATCH_OK room={} triggerId={} captures={} textLen={}", + room.getId(), + stack.triggerItem().getId(), + capturedValues.size(), + (text != null ? text.length() : 0)); + } + return CaptureResult.matched(capturedValues); } @@ -108,12 +133,13 @@ public final class WiredTextInputCaptureSupport { return capturers; } - private static MatchResult matchTemplate(WiredTriggerHabboSaysKeyword trigger, String rawText, Map capturersByName) { - String text = rawText != null ? rawText.trim() : ""; + private static MatchResult matchTemplate(WiredTriggerHabboSaysKeyword trigger, String rawText, Map capturersByName, Room room) { + String text = rawText != null ? rawText : ""; + String normalizedText = text.trim(); String template = trigger.getKey() != null ? trigger.getKey().trim() : ""; if (trigger.getMatchMode() == MATCH_ALL_WORDS && template.isEmpty()) { - if (capturersByName.size() != 1 || text.isEmpty()) { + if (capturersByName.size() != 1 || normalizedText.isEmpty()) { return MatchResult.noMatch(); } @@ -123,12 +149,24 @@ public final class WiredTextInputCaptureSupport { return MatchResult.matched(captures); } + MatchResult adjacentCaptureResult = matchAdjacentCapturers(template, rawText, capturersByName, room, trigger.getMatchMode()); + if (adjacentCaptureResult != null) { + if (WiredManager.isDebugEnabled()) { + WiredManager.debug("[TextCapture] ADJACENT mode used key='{}' textLen={} matched={}", + safeForLog(template), + (rawText != null ? rawText.length() : 0), + adjacentCaptureResult.matches); + } + return adjacentCaptureResult; + } + TemplatePattern pattern = buildPattern(template); if (pattern == null) { return MatchResult.noMatch(); } - Matcher matcher = pattern.pattern.matcher(text); + String matchText = pattern.placeholderNames.isEmpty() ? normalizedText : text; + Matcher matcher = pattern.pattern.matcher(matchText); boolean matches = (trigger.getMatchMode() == MATCH_CONTAINS) ? matcher.find() : matcher.matches(); if (!matches) { return MatchResult.noMatch(); @@ -142,12 +180,136 @@ public final class WiredTextInputCaptureSupport { } String capturedValue = matcher.group(index + 1); - captures.put(placeholderName, capturedValue != null ? capturedValue.trim() : ""); + captures.put(placeholderName, normalizeCapturedValue(capturedValue)); } return MatchResult.matched(captures); } + private static MatchResult matchAdjacentCapturers(String template, String rawText, Map capturersByName, Room room, int matchMode) { + if (template == null || template.isEmpty() || rawText == null || capturersByName == null || capturersByName.isEmpty() || room == null) { + return null; + } + + Matcher matcher = PLACEHOLDER_PATTERN.matcher(template); + List placeholderNames = new ArrayList<>(); + int cursor = 0; + + while (matcher.find()) { + if (matcher.start() != cursor) { + return null; + } + + String placeholderName = matcher.group(1) != null ? matcher.group(1).trim().toLowerCase() : ""; + if (placeholderName.isEmpty() || !capturersByName.containsKey(placeholderName)) { + return null; + } + + placeholderNames.add(placeholderName); + cursor = matcher.end(); + } + + if (placeholderNames.isEmpty() || cursor != template.length()) { + return null; + } + + int placeholderCount = placeholderNames.size(); + int textLength = rawText.length(); + + boolean[][] reachable = new boolean[placeholderCount + 1][textLength + 1]; + int[][] previousIndex = new int[placeholderCount + 1][textLength + 1]; + String[][] capturedValues = new String[placeholderCount + 1][textLength + 1]; + + for (int placeholderIndex = 0; placeholderIndex <= placeholderCount; placeholderIndex++) { + for (int textIndex = 0; textIndex <= textLength; textIndex++) { + previousIndex[placeholderIndex][textIndex] = -1; + } + } + + reachable[0][0] = true; + + for (int placeholderIndex = 0; placeholderIndex < placeholderCount; placeholderIndex++) { + String placeholderName = placeholderNames.get(placeholderIndex); + WiredExtraTextInputVariable capturer = capturersByName.get(placeholderName); + if (capturer == null) { + return MatchResult.noMatch(); + } + + for (int textIndex = 0; textIndex <= textLength; textIndex++) { + if (!reachable[placeholderIndex][textIndex]) { + continue; + } + + int minEndIndex = (textIndex < textLength) ? (textIndex + 1) : textIndex; + for (int endIndex = minEndIndex; endIndex <= textLength; endIndex++) { + if (reachable[placeholderIndex + 1][endIndex]) { + continue; + } + + String candidate = rawText.substring(textIndex, endIndex); + if (capturer.resolveCapturedValue(room, candidate) == null) { + continue; + } + + reachable[placeholderIndex + 1][endIndex] = true; + previousIndex[placeholderIndex + 1][endIndex] = textIndex; + capturedValues[placeholderIndex + 1][endIndex] = candidate; + } + } + } + + int resultEndIndex = -1; + if (matchMode == MATCH_CONTAINS) { + for (int endIndex = textLength; endIndex >= 0; endIndex--) { + if (reachable[placeholderCount][endIndex]) { + resultEndIndex = endIndex; + break; + } + } + } else if (reachable[placeholderCount][textLength]) { + resultEndIndex = textLength; + } + + if (resultEndIndex < 0) { + return MatchResult.noMatch(); + } + + LinkedHashMap captures = new LinkedHashMap<>(); + int backtrackTextIndex = resultEndIndex; + for (int placeholderIndex = placeholderCount; placeholderIndex > 0; placeholderIndex--) { + String placeholderName = placeholderNames.get(placeholderIndex - 1); + String capturedValue = capturedValues[placeholderIndex][backtrackTextIndex]; + captures.put(placeholderName, capturedValue != null ? capturedValue : ""); + backtrackTextIndex = previousIndex[placeholderIndex][backtrackTextIndex]; + if (backtrackTextIndex < 0) { + return MatchResult.noMatch(); + } + } + + return MatchResult.matched(captures); + } + + private static String normalizeCapturedValue(String value) { + return value != null ? value : ""; + } + + private static String safeForLog(String value) { + if (value == null) { + return ""; + } + + String normalized = value + .replace("\r", "\\r") + .replace("\n", "\\n") + .replace("\u00A0", "⍽"); + + if (normalized.length() > 180) { + return normalized.substring(0, 180) + "...(" + normalized.length() + ")"; + } + + return normalized; + } + private static TemplatePattern buildPattern(String template) { if (template == null || template.isEmpty()) { return null; @@ -160,7 +322,7 @@ public final class WiredTextInputCaptureSupport { while (matcher.find()) { regex.append(Pattern.quote(template.substring(cursor, matcher.start()))); - regex.append("(.+?)"); + regex.append(hasPlaceholderAfter(template, matcher.end()) ? "(.+?)" : "(.+)"); String placeholderName = matcher.group(1) != null ? matcher.group(1).trim().toLowerCase() : ""; placeholderNames.add(placeholderName); @@ -176,6 +338,10 @@ public final class WiredTextInputCaptureSupport { return new TemplatePattern(Pattern.compile(regex.toString(), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE), placeholderNames); } + private static boolean hasPlaceholderAfter(String template, int cursor) { + return PLACEHOLDER_PATTERN.matcher(template.substring(cursor)).find(); + } + public static void applyToContext(WiredContext ctx, Room room, CaptureResult captureResult) { if (ctx == null || room == null || captureResult == null || !captureResult.matches || captureResult.capturedValues.isEmpty()) { return; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java index d1db42ab..4f3631b8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java @@ -32,6 +32,8 @@ import java.util.List; import java.util.Locale; public final class WiredTextPlaceholderUtil { + private static final char PRESERVED_SPACE = '\u00A0'; + private WiredTextPlaceholderUtil() { } @@ -87,7 +89,41 @@ public final class WiredTextPlaceholderUtil { } } - return resolvedText; + return preserveRepeatedSpaces(resolvedText); + } + + private static String preserveRepeatedSpaces(String text) { + if (text == null || text.length() < 2) { + return text; + } + + StringBuilder result = new StringBuilder(text.length()); + int index = 0; + while (index < text.length()) { + char currentChar = text.charAt(index); + if (currentChar != ' ') { + result.append(currentChar); + index++; + continue; + } + + int startIndex = index; + while (index < text.length() && text.charAt(index) == ' ') { + index++; + } + + int spaceCount = index - startIndex; + if (spaceCount == 1) { + result.append(' '); + continue; + } + + for (int spaceIndex = 0; spaceIndex < spaceCount; spaceIndex++) { + result.append(PRESERVED_SPACE); + } + } + + return result.toString(); } public static boolean requiresActor(Room room, HabboItem stackItem) { @@ -275,7 +311,7 @@ public final class WiredTextPlaceholderUtil { } String value = resolveRoomVariableValue(room, extra); - return (value == null || value.isEmpty()) ? List.of() : List.of(value); + return value == null ? List.of() : List.of(value); } private static List collectContextVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) { @@ -284,7 +320,7 @@ public final class WiredTextPlaceholderUtil { } String value = resolveContextVariableValue(ctx, extra); - return (value == null || value.isEmpty()) ? List.of() : List.of(value); + return value == null ? List.of() : List.of(value); } private static String resolveUserVariableValue(Room room, RoomUnit roomUnit, WiredExtraTextOutputVariable extra) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java index 38764d79..3b076e0b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java @@ -11,6 +11,8 @@ import java.util.List; import java.util.Map; public final class WiredVariableTextConnectorSupport { + private static final String PRESERVED_SPACE = "\u00A0"; + private WiredVariableTextConnectorSupport() { } @@ -71,7 +73,7 @@ public final class WiredVariableTextConnectorSupport { Map mappings = connector.getMappings(); if (mappings.containsKey(value)) { String mappedValue = mappings.get(value); - return mappedValue != null ? mappedValue : String.valueOf(value); + return mappedValue != null ? preserveSpaces(mappedValue) : ""; } } @@ -83,10 +85,7 @@ public final class WiredVariableTextConnectorSupport { return null; } - String normalizedText = text.trim(); - if (normalizedText.isEmpty()) { - return null; - } + String normalizedText = normalizePreservedSpaces(text); for (WiredExtraVariableTextConnector connector : getConnectors(room, definitionItemId)) { Integer mappedValue = connector.resolveValue(normalizedText); @@ -97,4 +96,12 @@ public final class WiredVariableTextConnectorSupport { return null; } + + private static String preserveSpaces(String value) { + return value.replace(" ", PRESERVED_SPACE); + } + + private static String normalizePreservedSpaces(String value) { + return value.replace(PRESERVED_SPACE, " "); + } } 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 e1e4d312..0d38b908 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -36,6 +36,7 @@ import com.eu.habbo.messages.incoming.helper.MySanctionStatusEvent; import com.eu.habbo.messages.incoming.helper.RequestTalentTrackEvent; import com.eu.habbo.messages.incoming.hotelview.*; import com.eu.habbo.messages.incoming.inventory.*; +import com.eu.habbo.messages.incoming.inventory.nickicons.*; import com.eu.habbo.messages.incoming.inventory.prefixes.*; import com.eu.habbo.messages.incoming.modtool.*; import com.eu.habbo.messages.incoming.navigator.*; @@ -61,6 +62,8 @@ import com.eu.habbo.messages.incoming.rooms.promotions.RequestPromotionRoomsEven import com.eu.habbo.messages.incoming.rooms.promotions.UpdateRoomPromotionEvent; import com.eu.habbo.messages.incoming.rooms.users.*; import com.eu.habbo.messages.incoming.trading.*; +import com.eu.habbo.messages.incoming.translation.TranslationLanguagesRequestEvent; +import com.eu.habbo.messages.incoming.translation.TranslationTextRequestEvent; import com.eu.habbo.messages.incoming.unknown.RequestResolutionEvent; import com.eu.habbo.messages.incoming.unknown.UnknownEvent1; import com.eu.habbo.messages.incoming.users.*; @@ -117,6 +120,7 @@ public class PacketManager { this.registerGuilds(); this.registerPets(); this.registerWired(); + this.registerTranslation(); this.registerAchievements(); this.registerFloorPlanEditor(); this.registerAmbassadors(); @@ -409,6 +413,13 @@ public class PacketManager { this.registerHandler(Incoming.SetActivePrefixEvent, SetActivePrefixEvent.class); this.registerHandler(Incoming.DeletePrefixEvent, DeletePrefixEvent.class); this.registerHandler(Incoming.PurchasePrefixEvent, PurchasePrefixEvent.class); + this.registerHandler(Incoming.PurchaseCatalogPrefixEvent, PurchaseCatalogPrefixEvent.class); + this.registerHandler(Incoming.SetDisplayOrderEvent, SetDisplayOrderEvent.class); + + // Nick Icons + this.registerHandler(Incoming.RequestUserNickIconsEvent, RequestUserNickIconsEvent.class); + this.registerHandler(Incoming.PurchaseNickIconEvent, PurchaseNickIconEvent.class); + this.registerHandler(Incoming.SetActiveNickIconEvent, SetActiveNickIconEvent.class); } void registerRooms() throws Exception { @@ -635,6 +646,11 @@ public class PacketManager { this.registerHandler(Incoming.WiredUserInspectMoveEvent, WiredUserInspectMoveEvent.class); } + void registerTranslation() throws Exception { + this.registerHandler(Incoming.TranslationLanguagesRequestEvent, TranslationLanguagesRequestEvent.class); + this.registerHandler(Incoming.TranslationTextRequestEvent, TranslationTextRequestEvent.class); + } + void registerUnknown() throws Exception { this.registerHandler(Incoming.RequestResolutionEvent, RequestResolutionEvent.class); this.registerHandler(Incoming.RequestTalenTrackEvent, RequestTalentTrackEvent.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 d2b24daf..77ef0064 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 @@ -419,6 +419,8 @@ public class Incoming { public static final int WiredUserVariableUpdateEvent = 10025; public static final int WiredUserVariableManageEvent = 10026; public static final int WiredUserInspectMoveEvent = 10027; + public static final int TranslationLanguagesRequestEvent = 10032; + public static final int TranslationTextRequestEvent = 10033; public static final int RequestInventoryPetDelete = 10030; public static final int RequestInventoryBadgeDelete = 10031; @@ -448,6 +450,11 @@ public class Incoming { public static final int SetActivePrefixEvent = 7012; public static final int DeletePrefixEvent = 7013; public static final int PurchasePrefixEvent = 7014; + public static final int RequestUserNickIconsEvent = 7015; + public static final int PurchaseNickIconEvent = 7016; + public static final int SetActiveNickIconEvent = 7017; + public static final int PurchaseCatalogPrefixEvent = 7018; + public static final int SetDisplayOrderEvent = 7019; // YouTube Room Broadcast public static final int YouTubeRoomPlayEvent = 8001; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/PurchaseNickIconEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/PurchaseNickIconEvent.java new file mode 100644 index 00000000..4aeaa3ca --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/PurchaseNickIconEvent.java @@ -0,0 +1,95 @@ +package com.eu.habbo.messages.incoming.inventory.nickicons; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserNickIcon; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; +import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer; +import com.eu.habbo.messages.outgoing.users.UserCurrencyComposer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class PurchaseNickIconEvent extends MessageHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(PurchaseNickIconEvent.class); + + @Override + public int getRatelimit() { + return 500; + } + + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + + if (habbo == null) { + return; + } + + String requestedIconKey = normalizeIconKey(this.packet.readString()); + + if (requestedIconKey.isEmpty()) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Invalid nick icon selected.")); + return; + } + + if (habbo.getInventory().getNickIconsComponent().getNickIconByKey(requestedIconKey) != null) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "You already own this nick icon.")); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT points, points_type, enabled FROM custom_nick_icons_catalog WHERE icon_key = ? LIMIT 1")) { + statement.setString(1, requestedIconKey); + + try (ResultSet set = statement.executeQuery()) { + if (!set.next() || !set.getBoolean("enabled")) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "This nick icon is not available.")); + return; + } + + int points = set.getInt("points"); + int pointsType = set.getInt("points_type"); + + if (points > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < points) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points.")); + return; + } + + if (points > 0) { + habbo.getHabboInfo().addCurrencyAmount(pointsType, -points); + this.client.sendResponse(new UserCurrencyComposer(habbo)); + } + + UserNickIcon nickIcon = new UserNickIcon(habbo.getHabboInfo().getId(), requestedIconKey); + nickIcon.run(); + habbo.getInventory().getNickIconsComponent().addNickIcon(nickIcon); + + this.client.sendResponse(new UserNickIconsComposer(habbo)); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Unable to purchase this nick icon right now.")); + } + } + + private String normalizeIconKey(String iconKey) { + if (iconKey == null) { + return ""; + } + + String normalized = iconKey.trim().toLowerCase(); + + if (normalized.endsWith(".gif")) { + normalized = normalized.substring(0, normalized.length() - 4); + } + + return normalized.matches("^[a-z0-9_-]+$") ? normalized : ""; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/RequestUserNickIconsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/RequestUserNickIconsEvent.java new file mode 100644 index 00000000..84d2344f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/RequestUserNickIconsEvent.java @@ -0,0 +1,11 @@ +package com.eu.habbo.messages.incoming.inventory.nickicons; + +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer; + +public class RequestUserNickIconsEvent extends MessageHandler { + @Override + public void handle() throws Exception { + this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo())); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/SetActiveNickIconEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/SetActiveNickIconEvent.java new file mode 100644 index 00000000..74717f83 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/nickicons/SetActiveNickIconEvent.java @@ -0,0 +1,34 @@ +package com.eu.habbo.messages.incoming.inventory.nickicons; + +import com.eu.habbo.habbohotel.users.UserNickIcon; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer; + +public class SetActiveNickIconEvent extends MessageHandler { + @Override + public void handle() throws Exception { + int nickIconId = this.packet.readInt(); + + if (nickIconId == 0) { + this.client.getHabbo().getInventory().getNickIconsComponent().deactivateAll(); + this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo())); + if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { + this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose()); + } + return; + } + + UserNickIcon nickIcon = this.client.getHabbo().getInventory().getNickIconsComponent().getNickIcon(nickIconId); + + if (nickIcon == null) { + return; + } + + this.client.getHabbo().getInventory().getNickIconsComponent().setActive(nickIconId); + this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo())); + if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { + this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose()); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchaseCatalogPrefixEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchaseCatalogPrefixEvent.java new file mode 100644 index 00000000..89c07a95 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchaseCatalogPrefixEvent.java @@ -0,0 +1,84 @@ +package com.eu.habbo.messages.incoming.inventory.prefixes; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserPrefix; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; +import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer; +import com.eu.habbo.messages.outgoing.users.UserCurrencyComposer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class PurchaseCatalogPrefixEvent extends MessageHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(PurchaseCatalogPrefixEvent.class); + + @Override + public int getRatelimit() { + return 500; + } + + @Override + public void handle() throws Exception { + int catalogPrefixId = this.packet.readInt(); + Habbo habbo = this.client.getHabbo(); + + if (habbo == null || catalogPrefixId <= 0) { + return; + } + + if (habbo.getInventory().getPrefixesComponent().getPrefixByCatalogId(catalogPrefixId) != null) { + this.client.sendResponse(new UserNickIconsComposer(habbo)); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT display_name, text, color, icon, effect, font, points, points_type FROM custom_prefixes_catalog WHERE id = ? AND enabled = 1 LIMIT 1")) { + statement.setInt(1, catalogPrefixId); + + try (ResultSet set = statement.executeQuery()) { + if (!set.next()) { + return; + } + + int points = set.getInt("points"); + int pointsType = set.getInt("points_type"); + + if (points > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < points) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points.")); + return; + } + + if (points > 0) { + habbo.getHabboInfo().addCurrencyAmount(pointsType, -points); + this.client.sendResponse(new UserCurrencyComposer(habbo)); + } + + UserPrefix prefix = new UserPrefix( + habbo.getHabboInfo().getId(), + set.getString("text"), + set.getString("color"), + set.getString("icon"), + set.getString("effect"), + set.getString("font"), + catalogPrefixId, + set.getString("display_name"), + points, + pointsType, + false); + prefix.run(); + habbo.getInventory().getPrefixesComponent().addPrefix(prefix); + this.client.sendResponse(new UserNickIconsComposer(habbo)); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception while purchasing catalog prefix", e); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchasePrefixEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchasePrefixEvent.java index 8e04000f..54fa81c8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchasePrefixEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchasePrefixEvent.java @@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.users.UserPrefix; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; +import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer; import com.eu.habbo.messages.outgoing.inventory.prefixes.PrefixReceivedComposer; import com.eu.habbo.messages.outgoing.users.UserCreditsComposer; import com.eu.habbo.messages.outgoing.users.UserCurrencyComposer; @@ -19,6 +20,7 @@ import java.sql.SQLException; public class PurchasePrefixEvent extends MessageHandler { private static final Logger LOGGER = LoggerFactory.getLogger(PurchasePrefixEvent.class); + private static final String[] ALLOWED_FONTS = { "", "pixel", "cherry", "vampiro" }; @Override public int getRatelimit() { @@ -31,6 +33,7 @@ public class PurchasePrefixEvent extends MessageHandler { String color = this.packet.readString(); String icon = this.packet.readString(); String effect = this.packet.readString(); + String font = this.packet.readString(); Habbo habbo = this.client.getHabbo(); @@ -42,6 +45,9 @@ public class PurchasePrefixEvent extends MessageHandler { int priceCredits = getSettingInt("price_credits", 5); int pricePoints = getSettingInt("price_points", 0); int pointsType = getSettingInt("points_type", 0); + int fontPriceCredits = getSettingInt("font_price_credits", 10); + int fontPricePoints = getSettingInt("font_price_points", 0); + int fontPointsType = getSettingInt("font_points_type", pointsType); // Validate text text = text.trim(); @@ -72,43 +78,67 @@ public class PurchasePrefixEvent extends MessageHandler { return; } + if (icon == null) icon = ""; + icon = icon.trim(); + + if (effect == null) effect = ""; + effect = effect.trim(); + + if (font == null) font = ""; + font = font.trim().toLowerCase(); + + if (!isAllowedFont(font)) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Invalid font format.")); + return; + } + + int totalPriceCredits = priceCredits + (!font.isEmpty() ? fontPriceCredits : 0); + // Check credits - if (priceCredits > 0 && habbo.getHabboInfo().getCredits() < priceCredits) { + if (totalPriceCredits > 0 && habbo.getHabboInfo().getCredits() < totalPriceCredits) { this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough credits.")); return; } + int totalPricePointsSameType = pricePoints + ((fontPricePoints > 0 && fontPointsType == pointsType && !font.isEmpty()) ? fontPricePoints : 0); + // Check points - if (pricePoints > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < pricePoints) { + if (totalPricePointsSameType > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < totalPricePointsSameType) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points.")); + return; + } + + if (!font.isEmpty() && fontPricePoints > 0 && fontPointsType != pointsType && habbo.getHabboInfo().getCurrencyAmount(fontPointsType) < fontPricePoints) { this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points.")); return; } // Deduct currency - if (priceCredits > 0) { - habbo.getHabboInfo().addCredits(-priceCredits); + if (totalPriceCredits > 0) { + habbo.getHabboInfo().addCredits(-totalPriceCredits); this.client.sendResponse(new UserCreditsComposer(habbo)); } - if (pricePoints > 0) { - habbo.getHabboInfo().addCurrencyAmount(pointsType, -pricePoints); + if (totalPricePointsSameType > 0) { + habbo.getHabboInfo().addCurrencyAmount(pointsType, -totalPricePointsSameType); this.client.sendResponse(new UserCurrencyComposer(habbo)); } - // Validate icon (allow empty or known icon names) - if (icon == null) icon = ""; - icon = icon.trim(); - - // Validate effect - if (effect == null) effect = ""; - effect = effect.trim(); + if (!font.isEmpty() && fontPricePoints > 0 && fontPointsType != pointsType) { + habbo.getHabboInfo().addCurrencyAmount(fontPointsType, -fontPricePoints); + this.client.sendResponse(new UserCurrencyComposer(habbo)); + } // Create prefix - UserPrefix prefix = new UserPrefix(habbo.getHabboInfo().getId(), text, color, icon, effect); + int storedPoints = totalPricePointsSameType; + int storedPointsType = (storedPoints > 0) ? pointsType : ((!font.isEmpty() && fontPricePoints > 0) ? fontPointsType : pointsType); + + UserPrefix prefix = new UserPrefix(habbo.getHabboInfo().getId(), text, color, icon, effect, font, 0, text, storedPoints, storedPointsType, true); prefix.run(); // Insert into DB synchronously to get the ID habbo.getInventory().getPrefixesComponent().addPrefix(prefix); this.client.sendResponse(new PrefixReceivedComposer(prefix)); + this.client.sendResponse(new UserNickIconsComposer(habbo)); } private int getSettingInt(String key, int defaultValue) { @@ -142,4 +172,14 @@ public class PurchasePrefixEvent extends MessageHandler { } return false; } + + private boolean isAllowedFont(String font) { + for (String allowedFont : ALLOWED_FONTS) { + if (allowedFont.equals(font)) { + return true; + } + } + + return false; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetActivePrefixEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetActivePrefixEvent.java index 9ec5710a..16d88890 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetActivePrefixEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetActivePrefixEvent.java @@ -3,6 +3,8 @@ package com.eu.habbo.messages.incoming.inventory.prefixes; import com.eu.habbo.habbohotel.users.UserPrefix; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.inventory.prefixes.ActivePrefixUpdatedComposer; +import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer; public class SetActivePrefixEvent extends MessageHandler { @Override @@ -12,6 +14,11 @@ public class SetActivePrefixEvent extends MessageHandler { if (prefixId == 0) { this.client.getHabbo().getInventory().getPrefixesComponent().deactivateAll(); this.client.sendResponse(new ActivePrefixUpdatedComposer(null)); + this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo())); + + if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { + this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose()); + } return; } @@ -21,5 +28,10 @@ public class SetActivePrefixEvent extends MessageHandler { this.client.getHabbo().getInventory().getPrefixesComponent().setActive(prefixId); this.client.sendResponse(new ActivePrefixUpdatedComposer(prefix)); + this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo())); + + if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { + this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose()); + } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetDisplayOrderEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetDisplayOrderEvent.java new file mode 100644 index 00000000..b1406b15 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetDisplayOrderEvent.java @@ -0,0 +1,26 @@ +package com.eu.habbo.messages.incoming.inventory.prefixes; + +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.inventory.UserVisualSettingsComponent; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer; + +public class SetDisplayOrderEvent extends MessageHandler { + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + + if (habbo == null || habbo.getInventory() == null || habbo.getInventory().getUserVisualSettingsComponent() == null) { + return; + } + + String displayOrder = UserVisualSettingsComponent.sanitizeDisplayOrder(this.packet.readString()); + habbo.getInventory().getUserVisualSettingsComponent().setDisplayOrder(displayOrder); + this.client.sendResponse(new UserNickIconsComposer(habbo)); + + if (habbo.getHabboInfo().getCurrentRoom() != null) { + habbo.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(habbo).compose()); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/translation/TranslationLanguagesRequestEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/translation/TranslationLanguagesRequestEvent.java new file mode 100644 index 00000000..6ec5bb91 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/translation/TranslationLanguagesRequestEvent.java @@ -0,0 +1,28 @@ +package com.eu.habbo.messages.incoming.translation; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.translations.GoogleTranslateManager; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.translation.TranslationLanguagesComposer; + +public class TranslationLanguagesRequestEvent extends MessageHandler { + @Override + public void handle() { + final GameClient client = this.client; + final String displayLanguage = this.packet.readString(); + + Emulator.getThreading().run(() -> { + GoogleTranslateManager.SupportedLanguagesResponse response = Emulator.getGameEnvironment() + .getGoogleTranslateManager() + .getSupportedLanguages(displayLanguage); + + client.sendResponse(new TranslationLanguagesComposer(response).compose()); + }); + } + + @Override + public int getRatelimit() { + return 250; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/translation/TranslationTextRequestEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/translation/TranslationTextRequestEvent.java new file mode 100644 index 00000000..798e97ce --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/translation/TranslationTextRequestEvent.java @@ -0,0 +1,25 @@ +package com.eu.habbo.messages.incoming.translation; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.translations.GoogleTranslateManager; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.translation.TranslationResultComposer; + +public class TranslationTextRequestEvent extends MessageHandler { + @Override + public void handle() { + final GameClient client = this.client; + final int requestId = this.packet.readInt(); + final String text = this.packet.readString(); + final String targetLanguage = this.packet.readString(); + + Emulator.getThreading().run(() -> { + GoogleTranslateManager.TranslationResponse response = Emulator.getGameEnvironment() + .getGoogleTranslateManager() + .translate(text, targetLanguage); + + client.sendResponse(new TranslationResultComposer(requestId, response).compose()); + }); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java index 258e02ce..85728ba7 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java @@ -9,6 +9,7 @@ import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.generic.alerts.UpdateFailedComposer; +import com.eu.habbo.messages.outgoing.rooms.items.ItemStateComposer; import com.eu.habbo.messages.outgoing.wired.WiredSavedComposer; public class WiredEffectSaveDataEvent extends MessageHandler { @@ -39,6 +40,16 @@ public class WiredEffectSaveDataEvent extends MessageHandler { if (saved) { this.client.sendResponse(new WiredSavedComposer()); if (effect != null) { + if (effect.isSelector()) { + if (effect.usesExistingSelectorTargets()) { + effect.setExtradata("3"); + room.sendComposer(new ItemStateComposer(effect).compose()); + } else if ("3".equals(effect.getExtradata()) || "4".equals(effect.getExtradata()) || "5".equals(effect.getExtradata())) { + effect.setExtradata("0"); + room.sendComposer(new ItemStateComposer(effect).compose()); + } + } + effect.needsUpdate(true); Emulator.getThreading().run(effect); } else { 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 9a28a3a6..45d8c986 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 @@ -124,6 +124,8 @@ public class Outgoing { public final static int WiredRoomSettingsDataComposer = 5102; // CUSTOM public final static int WiredUserVariablesDataComposer = 5103; // CUSTOM public final static int ConfInvisStateComposer = 5104; // CUSTOM + public final static int TranslationLanguagesComposer = 5106; // CUSTOM + public final static int TranslationResultComposer = 5107; // CUSTOM public final static int AreaHideComposer = 6001; // CUSTOM public final static int RoomPaintComposer = 2454; // PRODUCTION-201611291003-338511768 public final static int MarketplaceConfigComposer = 1823; // PRODUCTION-201611291003-338511768 @@ -576,6 +578,7 @@ public class Outgoing { public static final int UserPrefixesComposer = 7001; public static final int PrefixReceivedComposer = 7002; public static final int ActivePrefixUpdatedComposer = 7003; + public static final int UserNickIconsComposer = 7004; public static final int AvailableCommandsComposer = 4050; // YouTube Room Broadcast diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/nickicons/UserNickIconsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/nickicons/UserNickIconsComposer.java new file mode 100644 index 00000000..552a4fa9 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/nickicons/UserNickIconsComposer.java @@ -0,0 +1,217 @@ +package com.eu.habbo.messages.outgoing.inventory.nickicons; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserCustomizationData; +import com.eu.habbo.habbohotel.users.UserNickIcon; +import com.eu.habbo.habbohotel.users.UserPrefix; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class UserNickIconsComposer extends MessageComposer { + private static final Logger LOGGER = LoggerFactory.getLogger(UserNickIconsComposer.class); + + private final Habbo habbo; + + public UserNickIconsComposer(Habbo habbo) { + this.habbo = habbo; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.UserNickIconsComposer); + + if (this.habbo == null || this.habbo.getInventory() == null || this.habbo.getInventory().getNickIconsComponent() == null) { + this.response.appendInt(0); + return this.response; + } + + Map ownedByKey = new HashMap<>(); + List ownedNickIcons = this.habbo.getInventory().getNickIconsComponent().getNickIcons(); + + for (UserNickIcon nickIcon : ownedNickIcons) { + ownedByKey.put(nickIcon.getIconKey().toLowerCase(), nickIcon); + } + + Map ownedPrefixesByCatalogId = new HashMap<>(); + List ownedPrefixes = this.habbo.getInventory().getPrefixesComponent().getPrefixes(); + + for (UserPrefix prefix : ownedPrefixes) { + if (prefix.getCatalogPrefixId() > 0) { + ownedPrefixesByCatalogId.put(prefix.getCatalogPrefixId(), prefix); + } + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT icon_key, display_name, points, points_type FROM custom_nick_icons_catalog WHERE enabled = 1 ORDER BY sort_order ASC, id ASC")) { + try (ResultSet set = statement.executeQuery()) { + List catalogNickIcons = new ArrayList<>(); + + while (set.next()) { + catalogNickIcons.add(new CatalogNickIcon( + set.getString("icon_key"), + set.getString("display_name"), + set.getInt("points"), + set.getInt("points_type"))); + } + + this.response.appendInt(catalogNickIcons.size()); + + for (CatalogNickIcon catalogNickIcon : catalogNickIcons) { + UserNickIcon ownedNickIcon = ownedByKey.get(catalogNickIcon.iconKey.toLowerCase()); + + this.response.appendString(catalogNickIcon.iconKey); + this.response.appendString(catalogNickIcon.displayName != null ? catalogNickIcon.displayName : ""); + this.response.appendInt(catalogNickIcon.points); + this.response.appendInt(catalogNickIcon.pointsType); + this.response.appendInt(ownedNickIcon != null ? 1 : 0); + this.response.appendInt((ownedNickIcon != null && ownedNickIcon.isActive()) ? 1 : 0); + this.response.appendInt(ownedNickIcon != null ? ownedNickIcon.getId() : 0); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + this.response.appendInt(0); + } + + UserCustomizationData customizationData = UserCustomizationData.fromHabbo(this.habbo); + this.response.appendString(customizationData.displayOrder); + this.response.appendInt(this.getSettingInt("max_length", 15)); + this.response.appendInt(this.getSettingInt("price_credits", 5)); + this.response.appendInt(this.getSettingInt("price_points", 0)); + this.response.appendInt(this.getSettingInt("points_type", 0)); + this.response.appendInt(this.getSettingInt("font_price_credits", 10)); + this.response.appendInt(this.getSettingInt("font_price_points", 0)); + this.response.appendInt(this.getSettingInt("font_points_type", 0)); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT id, display_name, text, color, icon, effect, font, points, points_type FROM custom_prefixes_catalog WHERE enabled = 1 ORDER BY sort_order ASC, id ASC")) { + try (ResultSet set = statement.executeQuery()) { + List catalogPrefixes = new ArrayList<>(); + + while (set.next()) { + catalogPrefixes.add(new CatalogPrefix( + set.getInt("id"), + set.getString("display_name"), + set.getString("text"), + set.getString("color"), + set.getString("icon"), + set.getString("effect"), + set.getString("font"), + set.getInt("points"), + set.getInt("points_type"))); + } + + this.response.appendInt(catalogPrefixes.size()); + + for (CatalogPrefix catalogPrefix : catalogPrefixes) { + UserPrefix ownedPrefix = ownedPrefixesByCatalogId.get(catalogPrefix.id); + + this.response.appendInt(catalogPrefix.id); + this.response.appendString(catalogPrefix.displayName != null ? catalogPrefix.displayName : catalogPrefix.text); + this.response.appendString(catalogPrefix.text != null ? catalogPrefix.text : ""); + this.response.appendString(catalogPrefix.color != null ? catalogPrefix.color : ""); + this.response.appendString(catalogPrefix.icon != null ? catalogPrefix.icon : ""); + this.response.appendString(catalogPrefix.effect != null ? catalogPrefix.effect : ""); + this.response.appendString(catalogPrefix.font != null ? catalogPrefix.font : ""); + this.response.appendInt(catalogPrefix.points); + this.response.appendInt(catalogPrefix.pointsType); + this.response.appendInt(ownedPrefix != null ? 1 : 0); + this.response.appendInt((ownedPrefix != null && ownedPrefix.isActive()) ? 1 : 0); + this.response.appendInt(ownedPrefix != null ? ownedPrefix.getId() : 0); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception loading prefix catalog", e); + this.response.appendInt(0); + } + + this.response.appendInt(ownedPrefixes.size()); + + for (UserPrefix prefix : ownedPrefixes) { + this.response.appendInt(prefix.getId()); + this.response.appendString(prefix.getDisplayName() != null ? prefix.getDisplayName() : prefix.getText()); + this.response.appendString(prefix.getText()); + this.response.appendString(prefix.getColor()); + this.response.appendString(prefix.getIcon()); + this.response.appendString(prefix.getEffect()); + this.response.appendString(prefix.getFont()); + this.response.appendInt(prefix.isActive() ? 1 : 0); + this.response.appendInt(prefix.isCustom() ? 1 : 0); + this.response.appendInt(prefix.getPoints()); + this.response.appendInt(prefix.getPointsType()); + this.response.appendInt(prefix.getCatalogPrefixId()); + } + + return this.response; + } + + private int getSettingInt(String key, int defaultValue) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT `value` FROM custom_prefix_settings WHERE key_name = ? LIMIT 1")) { + statement.setString(1, key); + + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + return Integer.parseInt(set.getString("value")); + } + } + } catch (SQLException | NumberFormatException e) { + LOGGER.error("Caught exception while resolving prefix setting {}", key, e); + } + + return defaultValue; + } + + private static class CatalogNickIcon { + private final String iconKey; + private final String displayName; + private final int points; + private final int pointsType; + + private CatalogNickIcon(String iconKey, String displayName, int points, int pointsType) { + this.iconKey = iconKey; + this.displayName = displayName; + this.points = points; + this.pointsType = pointsType; + } + } + + private static class CatalogPrefix { + private final int id; + private final String displayName; + private final String text; + private final String color; + private final String icon; + private final String effect; + private final String font; + private final int points; + private final int pointsType; + + private CatalogPrefix(int id, String displayName, String text, String color, String icon, String effect, String font, int points, int pointsType) { + this.id = id; + this.displayName = displayName; + this.text = text; + this.color = color; + this.icon = icon; + this.effect = effect; + this.font = font; + this.points = points; + this.pointsType = pointsType; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/ActivePrefixUpdatedComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/ActivePrefixUpdatedComposer.java index 13017e93..b78977e8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/ActivePrefixUpdatedComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/ActivePrefixUpdatedComposer.java @@ -22,12 +22,14 @@ public class ActivePrefixUpdatedComposer extends MessageComposer { this.response.appendString(this.prefix.getColor()); this.response.appendString(this.prefix.getIcon()); this.response.appendString(this.prefix.getEffect()); + this.response.appendString(this.prefix.getFont()); } else { this.response.appendInt(0); this.response.appendString(""); this.response.appendString(""); this.response.appendString(""); this.response.appendString(""); + this.response.appendString(""); } return this.response; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/PrefixReceivedComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/PrefixReceivedComposer.java index 98bdf055..6db2effe 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/PrefixReceivedComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/PrefixReceivedComposer.java @@ -20,6 +20,7 @@ public class PrefixReceivedComposer extends MessageComposer { this.response.appendString(this.prefix.getColor()); this.response.appendString(this.prefix.getIcon()); this.response.appendString(this.prefix.getEffect()); + this.response.appendString(this.prefix.getFont()); return this.response; } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/UserPrefixesComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/UserPrefixesComposer.java index 747e63b6..c75c2fe2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/UserPrefixesComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/UserPrefixesComposer.java @@ -30,6 +30,7 @@ public class UserPrefixesComposer extends MessageComposer { this.response.appendString(prefix.getColor()); this.response.appendString(prefix.getIcon()); this.response.appendString(prefix.getEffect()); + this.response.appendString(prefix.getFont()); this.response.appendInt(prefix.isActive() ? 1 : 0); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java index 755fdcc9..7162dbf0 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java @@ -1,6 +1,7 @@ package com.eu.habbo.messages.outgoing.rooms.users; import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserCustomizationData; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.Outgoing; @@ -24,6 +25,14 @@ public class RoomUserDataComposer extends MessageComposer { this.response.appendInt(this.habbo.getHabboInfo().getInfostandStand()); this.response.appendInt(this.habbo.getHabboInfo().getInfostandOverlay()); this.response.appendInt(this.habbo.getHabboInfo().getInfostandCardBg()); + UserCustomizationData customizationData = UserCustomizationData.fromHabbo(this.habbo); + this.response.appendString(customizationData.nickIcon); + this.response.appendString(customizationData.prefixText); + this.response.appendString(customizationData.prefixColor); + this.response.appendString(customizationData.prefixIcon); + this.response.appendString(customizationData.prefixEffect); + this.response.appendString(customizationData.prefixFont); + this.response.appendString(customizationData.displayOrder); return this.response; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java index cf878af8..796935c2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java @@ -4,6 +4,7 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.bots.Bot; import com.eu.habbo.habbohotel.guilds.Guild; import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserCustomizationData; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.Outgoing; @@ -67,6 +68,14 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendString(""); this.response.appendInt(this.habbo.getHabboStats().getAchievementScore()); this.response.appendBoolean(true); + UserCustomizationData customizationData = UserCustomizationData.fromHabbo(this.habbo); + this.response.appendString(customizationData.nickIcon); + this.response.appendString(customizationData.prefixText); + this.response.appendString(customizationData.prefixColor); + this.response.appendString(customizationData.prefixIcon); + this.response.appendString(customizationData.prefixEffect); + this.response.appendString(customizationData.prefixFont); + this.response.appendString(customizationData.displayOrder); this.response.appendString(this.habbo.getHabboInfo().getRoomEntryMethod()); this.response.appendInt(this.habbo.getHabboInfo().getRoomEntryTeleportId()); } else if (this.habbos != null) { @@ -101,6 +110,14 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendString(""); this.response.appendInt(habbo.getHabboStats().getAchievementScore()); this.response.appendBoolean(true); + UserCustomizationData customizationData = UserCustomizationData.fromHabbo(habbo); + this.response.appendString(customizationData.nickIcon); + this.response.appendString(customizationData.prefixText); + this.response.appendString(customizationData.prefixColor); + this.response.appendString(customizationData.prefixIcon); + this.response.appendString(customizationData.prefixEffect); + this.response.appendString(customizationData.prefixFont); + this.response.appendString(customizationData.displayOrder); this.response.appendString(habbo.getHabboInfo().getRoomEntryMethod()); this.response.appendInt(habbo.getHabboInfo().getRoomEntryTeleportId()); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/translation/TranslationLanguagesComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/translation/TranslationLanguagesComposer.java new file mode 100644 index 00000000..63d9de56 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/translation/TranslationLanguagesComposer.java @@ -0,0 +1,33 @@ +package com.eu.habbo.messages.outgoing.translation; + +import com.eu.habbo.habbohotel.translations.GoogleTranslateManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class TranslationLanguagesComposer extends MessageComposer { + private final GoogleTranslateManager.SupportedLanguagesResponse responseData; + + public TranslationLanguagesComposer(GoogleTranslateManager.SupportedLanguagesResponse responseData) { + this.responseData = responseData; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.TranslationLanguagesComposer); + this.response.appendBoolean(this.responseData != null && this.responseData.isSuccess()); + this.response.appendString(this.responseData != null ? this.responseData.getErrorMessage() : "Unknown error"); + + int count = (this.responseData != null) ? this.responseData.getLanguages().size() : 0; + this.response.appendInt(count); + + if (this.responseData != null) { + for (GoogleTranslateManager.SupportedLanguage language : this.responseData.getLanguages()) { + this.response.appendString(language.getCode()); + this.response.appendString(language.getName()); + } + } + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/translation/TranslationResultComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/translation/TranslationResultComposer.java new file mode 100644 index 00000000..662e81d5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/translation/TranslationResultComposer.java @@ -0,0 +1,29 @@ +package com.eu.habbo.messages.outgoing.translation; + +import com.eu.habbo.habbohotel.translations.GoogleTranslateManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class TranslationResultComposer extends MessageComposer { + private final int requestId; + private final GoogleTranslateManager.TranslationResponse responseData; + + public TranslationResultComposer(int requestId, GoogleTranslateManager.TranslationResponse responseData) { + this.requestId = requestId; + this.responseData = responseData; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.TranslationResultComposer); + this.response.appendInt(this.requestId); + this.response.appendBoolean(this.responseData != null && this.responseData.isSuccess()); + this.response.appendString(this.responseData != null ? this.responseData.getErrorMessage() : "Unknown error"); + this.response.appendString(this.responseData != null ? this.responseData.getOriginalText() : ""); + this.response.appendString(this.responseData != null ? this.responseData.getTranslatedText() : ""); + this.response.appendString(this.responseData != null ? this.responseData.getDetectedLanguage() : ""); + this.response.appendString(this.responseData != null ? this.responseData.getTargetLanguage() : ""); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java index 0a6fb0e7..e56326d4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java @@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.guilds.Guild; import com.eu.habbo.habbohotel.messenger.Messenger; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboInfo; +import com.eu.habbo.habbohotel.users.UserCustomizationData; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.Outgoing; @@ -116,6 +117,14 @@ public class UserProfileComposer extends MessageComposer { this.response.appendInt(this.habboInfo.getInfostandStand()); this.response.appendInt(this.habboInfo.getInfostandOverlay()); this.response.appendInt(this.habboInfo.getInfostandCardBg()); + UserCustomizationData customizationData = (this.habbo != null) ? UserCustomizationData.fromHabbo(this.habbo) : UserCustomizationData.fromUserId(this.habboInfo.getId()); + this.response.appendString(customizationData.nickIcon); + this.response.appendString(customizationData.prefixText); + this.response.appendString(customizationData.prefixColor); + this.response.appendString(customizationData.prefixIcon); + this.response.appendString(customizationData.prefixEffect); + this.response.appendString(customizationData.prefixFont); + this.response.appendString(customizationData.displayOrder); return this.response; } diff --git a/Emulator/src/main/java/com/eu/habbo/networking/Server.java b/Emulator/src/main/java/com/eu/habbo/networking/Server.java index f7a4a3ce..7ae6f43f 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/Server.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/Server.java @@ -92,4 +92,4 @@ public abstract class Server { public int getPort() { return this.port; } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java index 4558d9af..facfb285 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java @@ -3,6 +3,8 @@ package com.eu.habbo.networking.gameserver; import com.eu.habbo.Emulator; import com.eu.habbo.messages.PacketManager; import com.eu.habbo.networking.gameserver.auth.AuthHttpHandler; +import com.eu.habbo.networking.gameserver.auth.NitroSecureApiHandler; +import com.eu.habbo.networking.gameserver.auth.NitroSecureAssetHandler; import com.eu.habbo.networking.gameserver.badges.BadgeHttpHandler; import com.eu.habbo.networking.gameserver.codec.WebSocketCodec; import com.eu.habbo.networking.gameserver.crypto.WsHandshakeHandler; @@ -53,6 +55,8 @@ public class WebSocketChannelInitializer extends ChannelInitializer> SECURE_CONTEXTS = + AttributeKey.valueOf("nitroSecureApiContexts"); + private static final ConcurrentHashMap NONCE_CACHE = new ConcurrentHashMap<>(); + private static final long MAX_REQUEST_SKEW_MS = 90_000L; + private static final long NONCE_TTL_MS = 2 * 60 * 1000L; + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (!(msg instanceof FullHttpRequest req)) { + super.channelRead(ctx, msg); + return; + } + + String path = new QueryStringDecoder(req.uri()).path(); + + if (!secureApiEnabled()) { + super.channelRead(ctx, msg); + return; + } + + if (!path.startsWith(API_PREFIX)) { + super.channelRead(ctx, msg); + return; + } + + if (req.method() == HttpMethod.OPTIONS) { + sendCors(ctx, req); + ReferenceCountUtil.release(req); + return; + } + + if (!isSecureRequest(req)) { + super.channelRead(ctx, msg); + return; + } + + try { + String clientKey = req.headers().get("X-Nitro-Key"); + if (clientKey == null || clientKey.isBlank()) { + sendText(ctx, req, HttpResponseStatus.UNAUTHORIZED, "Missing key."); + return; + } + + SecretKey sessionKey = NitroSecureAssetHandler.deriveSessionKey(java.util.Base64.getDecoder().decode(clientKey)); + SecureApiContext secureContext = new SecureApiContext( + NitroSecureAssetHandler.getServerKeyFingerprint(), + NitroSecureAssetHandler.fingerprint(sessionKey.getEncoded()), + sessionKey + ); + + if (!req.content().isReadable()) { + enqueueContext(ctx, secureContext); + super.channelRead(ctx, msg); + return; + } + + byte[] encrypted = new byte[req.content().readableBytes()]; + req.content().getBytes(req.content().readerIndex(), encrypted); + byte[] clear = NitroSecureAssetHandler.decrypt(sessionKey, NitroSecureAssetHandler.fromHex(new String(encrypted, StandardCharsets.UTF_8))); + clear = unwrapEnvelope(clear, req, secureContext); + + FullHttpRequest decryptedReq = new DefaultFullHttpRequest( + req.protocolVersion(), + req.method(), + req.uri(), + Unpooled.wrappedBuffer(clear) + ); + + decryptedReq.headers().setAll(req.headers()); + decryptedReq.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8"); + decryptedReq.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, clear.length); + + enqueueContext(ctx, secureContext); + ReferenceCountUtil.release(req); + ctx.fireChannelRead(decryptedReq); + } catch (IllegalArgumentException e) { + LOGGER.warn("Nitro secure API rejected invalid encrypted payload", e); + sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, e.getMessage()); + ReferenceCountUtil.release(req); + } catch (Exception e) { + LOGGER.error("Nitro secure API failed to decrypt request", e); + sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, "Invalid secure payload."); + ReferenceCountUtil.release(req); + } + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (!(msg instanceof FullHttpResponse response)) { + super.write(ctx, msg, promise); + return; + } + + SecureApiContext secureContext = pollContext(ctx); + if (secureContext == null) { + super.write(ctx, msg, promise); + return; + } + + try { + byte[] clear = readBytes(response.content()); + byte[] encrypted = NitroSecureAssetHandler.encrypt(secureContext.sessionKey(), clear); + byte[] hex = NitroSecureAssetHandler.toHex(encrypted).getBytes(StandardCharsets.UTF_8); + + FullHttpResponse encryptedResponse = new DefaultFullHttpResponse( + response.protocolVersion(), + response.status(), + Unpooled.wrappedBuffer(hex) + ); + + encryptedResponse.headers().setAll(response.headers()); + encryptedResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8"); + encryptedResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, hex.length); + encryptedResponse.headers().set("X-Nitro-Sec", "1"); + encryptedResponse.headers().set("X-Nitro-Key-Fp", secureContext.serverKeyFingerprint()); + encryptedResponse.headers().set("X-Nitro-Derive-Fp", secureContext.derivedFingerprint()); + encryptedResponse.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp"); + + ReferenceCountUtil.release(response); + super.write(ctx, encryptedResponse, promise); + } catch (Exception e) { + LOGGER.error("Nitro secure API failed to encrypt response", e); + super.write(ctx, msg, promise); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + Deque contexts = ctx.channel().attr(SECURE_CONTEXTS).get(); + if (contexts != null) contexts.clear(); + super.channelInactive(ctx); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + Deque contexts = ctx.channel().attr(SECURE_CONTEXTS).get(); + if (contexts != null) contexts.clear(); + super.exceptionCaught(ctx, cause); + } + + private static boolean isSecureRequest(FullHttpRequest req) { + return "1".equals(req.headers().get("X-Nitro-Api")); + } + + private static boolean secureApiEnabled() { + return com.eu.habbo.Emulator.getConfig().getBoolean(ENABLED_CONFIG, true); + } + + private static byte[] unwrapEnvelope(byte[] clear, FullHttpRequest req, SecureApiContext secureContext) { + if (!requiresReplayEnvelope(req.method())) return clear; + + JsonObject envelope = JsonParser.parseString(new String(clear, StandardCharsets.UTF_8)).getAsJsonObject(); + long ts = envelope.has("ts") ? envelope.get("ts").getAsLong() : 0L; + String nonce = envelope.has("nonce") ? envelope.get("nonce").getAsString() : ""; + String method = envelope.has("method") ? envelope.get("method").getAsString() : ""; + String path = envelope.has("path") ? envelope.get("path").getAsString() : ""; + String body = envelope.has("body") ? envelope.get("body").getAsString() : ""; + long now = System.currentTimeMillis(); + + if (Math.abs(now - ts) > MAX_REQUEST_SKEW_MS) { + throw new IllegalArgumentException("Secure request expired."); + } + + if (!req.method().name().equalsIgnoreCase(method)) { + throw new IllegalArgumentException("Secure request method mismatch."); + } + + String requestPath = req.uri(); + if (!requestPath.equals(path)) { + throw new IllegalArgumentException("Secure request path mismatch."); + } + + if (nonce.isBlank()) { + throw new IllegalArgumentException("Missing secure request nonce."); + } + + cleanupExpiredNonces(now); + + String replayKey = secureContext.derivedFingerprint() + ':' + nonce; + if (NONCE_CACHE.putIfAbsent(replayKey, now + NONCE_TTL_MS) != null) { + throw new IllegalArgumentException("Secure request replay detected."); + } + + return java.util.Base64.getDecoder().decode(body); + } + + private static boolean requiresReplayEnvelope(HttpMethod method) { + return method == HttpMethod.POST + || method == HttpMethod.PUT + || method == HttpMethod.PATCH + || method == HttpMethod.DELETE; + } + + private static void cleanupExpiredNonces(long now) { + if (NONCE_CACHE.size() < 512) return; + NONCE_CACHE.entrySet().removeIf(entry -> entry.getValue() < now); + } + + private static void enqueueContext(ChannelHandlerContext ctx, SecureApiContext context) { + Deque queue = ctx.channel().attr(SECURE_CONTEXTS).get(); + if (queue == null) { + queue = new ArrayDeque<>(); + ctx.channel().attr(SECURE_CONTEXTS).set(queue); + } + + queue.addLast(context); + } + + private static SecureApiContext pollContext(ChannelHandlerContext ctx) { + Deque queue = ctx.channel().attr(SECURE_CONTEXTS).get(); + if (queue == null || queue.isEmpty()) return null; + return queue.pollFirst(); + } + + private static byte[] readBytes(ByteBuf content) { + byte[] bytes = new byte[content.readableBytes()]; + content.getBytes(content.readerIndex(), bytes); + return bytes; + } + + private static void sendCors(ChannelHandlerContext ctx, FullHttpRequest req) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT); + applyCors(req, response); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + private static void sendText(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, String text) { + byte[] bytes = text.getBytes(StandardCharsets.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes)); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + applyCors(req, response); + boolean keepAlive = isKeepAlive(req); + if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + var future = ctx.writeAndFlush(response); + if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE); + } + + private static void applyCors(FullHttpRequest req, FullHttpResponse response) { + String origin = req.headers().get(HttpHeaderNames.ORIGIN); + if (origin != null && !origin.isEmpty()) { + response.headers().set("Access-Control-Allow-Origin", origin); + response.headers().set("Vary", "Origin"); + response.headers().set("Access-Control-Allow-Credentials", "true"); + } + response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS"); + response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api"); + response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp"); + } + + private static boolean isKeepAlive(FullHttpRequest req) { + String connection = req.headers().get(HttpHeaderNames.CONNECTION); + return connection == null || !"close".equalsIgnoreCase(connection); + } + + private record SecureApiContext(String serverKeyFingerprint, String derivedFingerprint, SecretKey sessionKey) { + private SecureApiContext { + Objects.requireNonNull(serverKeyFingerprint); + Objects.requireNonNull(derivedFingerprint); + Objects.requireNonNull(sessionKey); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java new file mode 100644 index 00000000..a2da7b4e --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java @@ -0,0 +1,355 @@ +package com.eu.habbo.networking.gameserver.auth; + +import com.eu.habbo.Emulator; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.*; +import io.netty.util.ReferenceCountUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import javax.crypto.KeyAgreement; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.*; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class NitroSecureAssetHandler extends ChannelInboundHandlerAdapter { + private static final Logger LOGGER = LoggerFactory.getLogger(NitroSecureAssetHandler.class); + private static final String MASTER_KEY_CONFIG = "nitro.secure.master_key"; + private static final String ENABLED_CONFIG = "nitro.secure.assets.enabled"; + private static final String BOOTSTRAP_PATH = "/nitro-sec/bootstrap"; + private static final String FILE_PATH = "/nitro-sec/file"; + private static final int MAX_BOOTSTRAP_BODY_BYTES = 4096; + private static final SecureRandom RNG = new SecureRandom(); + private static final KeyPair SERVER_KEYPAIR = createServerKeyPair(); + private static final String SERVER_KEY_FINGERPRINT = fingerprint(SERVER_KEYPAIR.getPublic().getEncoded()); + private static final Map CACHE = new ConcurrentHashMap<>(); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (!(msg instanceof FullHttpRequest req)) { + super.channelRead(ctx, msg); + return; + } + + String path = new QueryStringDecoder(req.uri()).path(); + + if (!secureAssetsEnabled()) { + super.channelRead(ctx, msg); + return; + } + + if (!path.equals(BOOTSTRAP_PATH) && !path.equals(FILE_PATH)) { + super.channelRead(ctx, msg); + return; + } + + try { + if (req.method() == HttpMethod.OPTIONS) { + sendCors(ctx, req); + return; + } + + if (path.equals(BOOTSTRAP_PATH)) handleBootstrap(ctx, req); + else handleFile(ctx, req); + } finally { + ReferenceCountUtil.release(req); + } + } + + private void handleBootstrap(ChannelHandlerContext ctx, FullHttpRequest req) { + if (req.method() != HttpMethod.POST) { + sendText(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, "Use POST.", "text/plain; charset=utf-8"); + return; + } + + if (req.content().readableBytes() > MAX_BOOTSTRAP_BODY_BYTES) { + sendText(ctx, req, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, "Payload too large.", "text/plain; charset=utf-8"); + return; + } + + try { + JsonObject body = JsonParser.parseString(req.content().toString(StandardCharsets.UTF_8)).getAsJsonObject(); + String clientKey = body.has("key") ? body.get("key").getAsString() : ""; + if (clientKey.isEmpty()) { + sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, "Missing key.", "text/plain; charset=utf-8"); + return; + } + + JsonObject response = new JsonObject(); + response.addProperty("key", Base64.getEncoder().encodeToString(SERVER_KEYPAIR.getPublic().getEncoded())); + sendText(ctx, req, HttpResponseStatus.OK, response.toString(), "application/json; charset=utf-8"); + } catch (Exception e) { + LOGGER.warn("Nitro secure bootstrap failed", e); + sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, "Invalid bootstrap.", "text/plain; charset=utf-8"); + } + } + + private void handleFile(ChannelHandlerContext ctx, FullHttpRequest req) { + if (req.method() != HttpMethod.GET && req.method() != HttpMethod.HEAD) { + sendText(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, "Use GET.", "text/plain; charset=utf-8"); + return; + } + + QueryStringDecoder query = new QueryStringDecoder(req.uri()); + String clientKey = headerOrQuery(req, query, "X-Nitro-Key", "key"); + if (clientKey == null || clientKey.isEmpty()) { + sendText(ctx, req, HttpResponseStatus.UNAUTHORIZED, "Missing key.", "text/plain; charset=utf-8"); + return; + } + + String kind = queryParam(query, "kind"); + String file = queryParam(query, "file"); + if (!kind.equals("config") && !kind.equals("gamedata")) { + sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, "Invalid kind.", "text/plain; charset=utf-8"); + return; + } + + try { + SecretKey sessionKey = deriveSessionKey(Base64.getDecoder().decode(clientKey)); + byte[] clear = readAsset(kind, file); + byte[] encrypted = encrypt(sessionKey, clear); + sendText(ctx, req, HttpResponseStatus.OK, toHex(encrypted), "text/plain; charset=utf-8", true, fingerprint(sessionKey.getEncoded())); + } catch (IllegalArgumentException e) { + sendText(ctx, req, HttpResponseStatus.BAD_REQUEST, e.getMessage(), "text/plain; charset=utf-8"); + } catch (IOException e) { + sendText(ctx, req, HttpResponseStatus.NOT_FOUND, "Not found.", "text/plain; charset=utf-8"); + } catch (Exception e) { + LOGGER.error("Nitro secure asset failed kind=" + kind + " file=" + file, e); + sendText(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, "Server error.", "text/plain; charset=utf-8"); + } + } + + private static byte[] readAsset(String kind, String file) throws IOException { + String normalized = normalizeFile(file); + String rootConfigKey = kind.equals("config") ? "nitro.secure.config.root" : "nitro.secure.gamedata.root"; + String fallback = kind.equals("config") ? "Nitro-V3/public" : "Nitro-V3/public/nitro/gamedata"; + Path root = resolveRoot(rootConfigKey, fallback, kind.equals("config") + ? new String[] { "../Nitro-V3/public", "../../Nitro-V3/public", "Nitro-V3/public" } + : new String[] { "../Nitro-V3/public/nitro/gamedata", "../../Nitro-V3/public/nitro/gamedata", "Nitro-V3/public/nitro/gamedata" }); + Path target = root.resolve(normalized).normalize(); + + if (!target.startsWith(root)) throw new IllegalArgumentException("Invalid file."); + if (!Files.isRegularFile(target)) throw new IOException("Not found"); + + String cacheKey = kind + ":" + target; + long modified = Files.getLastModifiedTime(target).toMillis(); + CacheEntry cached = CACHE.get(cacheKey); + if (cached != null && cached.modified == modified) return cached.bytes; + + byte[] bytes = Files.readAllBytes(target); + if (normalized.toLowerCase().endsWith(".json")) bytes = minifyJson(bytes); + CACHE.put(cacheKey, new CacheEntry(modified, bytes)); + return bytes; + } + + private static String normalizeFile(String file) { + if (file == null) throw new IllegalArgumentException("Missing file."); + String value = URLDecoder.decode(file, StandardCharsets.UTF_8).replace('\\', '/'); + int queryIndex = value.indexOf('?'); + if (queryIndex >= 0) value = value.substring(0, queryIndex); + int fragmentIndex = value.indexOf('#'); + if (fragmentIndex >= 0) value = value.substring(0, fragmentIndex); + while (value.startsWith("/")) value = value.substring(1); + if (value.isEmpty() || value.contains("..") || value.contains(":")) throw new IllegalArgumentException("Invalid file."); + return value; + } + + private static byte[] minifyJson(byte[] bytes) { + try { + return JsonParser.parseString(new String(bytes, StandardCharsets.UTF_8)).toString().getBytes(StandardCharsets.UTF_8); + } catch (Exception ignored) { + return bytes; + } + } + + private static Path resolveRoot(String configKey, String fallback, String[] alternatives) { + String configured = Emulator.getConfig().getValue(configKey, ""); + if (configured != null && !configured.isEmpty()) return Path.of(configured).toAbsolutePath().normalize(); + + for (String alternative : alternatives) { + Path path = Path.of(alternative).toAbsolutePath().normalize(); + if (Files.isDirectory(path)) return path; + } + + return Path.of(fallback).toAbsolutePath().normalize(); + } + + private static boolean secureAssetsEnabled() { + return Emulator.getConfig().getBoolean(ENABLED_CONFIG, true); + } + + static SecretKey deriveSessionKey(byte[] clientPublicEncoded) throws Exception { + KeyFactory factory = KeyFactory.getInstance("EC"); + PublicKey clientPublic = factory.generatePublic(new X509EncodedKeySpec(clientPublicEncoded)); + KeyAgreement agreement = KeyAgreement.getInstance("ECDH"); + agreement.init(SERVER_KEYPAIR.getPrivate()); + agreement.doPhase(clientPublic, true); + byte[] secret = agreement.generateSecret(); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update(secret); + digest.update("nitro-secure-assets-v1".getBytes(StandardCharsets.UTF_8)); + return new SecretKeySpec(digest.digest(), "AES"); + } + + static byte[] encrypt(SecretKey key, byte[] clear) throws Exception { + byte[] iv = new byte[12]; + RNG.nextBytes(iv); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv)); + byte[] encrypted = cipher.doFinal(clear); + byte[] out = new byte[iv.length + encrypted.length]; + System.arraycopy(iv, 0, out, 0, iv.length); + System.arraycopy(encrypted, 0, out, iv.length, encrypted.length); + return out; + } + + static byte[] decrypt(SecretKey key, byte[] encryptedPayload) throws Exception { + if (encryptedPayload.length < 13) throw new IllegalArgumentException("Encrypted payload is too short."); + byte[] iv = new byte[12]; + byte[] payload = new byte[encryptedPayload.length - iv.length]; + System.arraycopy(encryptedPayload, 0, iv, 0, iv.length); + System.arraycopy(encryptedPayload, iv.length, payload, 0, payload.length); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv)); + return cipher.doFinal(payload); + } + + private static KeyPair createServerKeyPair() { + try { + String configuredSecret = Emulator.getConfig().getValue(MASTER_KEY_CONFIG, ""); + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); + if (configuredSecret != null && !configuredSecret.isBlank()) { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] seed = digest.digest(configuredSecret.getBytes(StandardCharsets.UTF_8)); + SecureRandom deterministic = SecureRandom.getInstance("SHA1PRNG"); + deterministic.setSeed(seed); + generator.initialize(256, deterministic); + LOGGER.info("Nitro secure assets using persistent server key from config {}", MASTER_KEY_CONFIG); + } else { + generator.initialize(256, RNG); + LOGGER.warn("Nitro secure assets using ephemeral server key because {} is empty", MASTER_KEY_CONFIG); + } + return generator.generateKeyPair(); + } catch (Exception e) { + throw new IllegalStateException("Unable to create Nitro secure server key", e); + } + } + + private static String headerOrQuery(FullHttpRequest req, QueryStringDecoder query, String header, String param) { + String value = req.headers().get(header); + return (value == null || value.isEmpty()) ? queryParam(query, param) : value; + } + + private static String queryParam(QueryStringDecoder query, String key) { + if (!query.parameters().containsKey(key) || query.parameters().get(key).isEmpty()) return ""; + return query.parameters().get(key).get(0); + } + + private static void sendText(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, String text, String contentType) { + sendBytes(ctx, req, status, text.getBytes(StandardCharsets.UTF_8), contentType, false, null); + } + + private static void sendText(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, String text, String contentType, boolean encrypted, String deriveFingerprint) { + sendBytes(ctx, req, status, text.getBytes(StandardCharsets.UTF_8), contentType, encrypted, deriveFingerprint); + } + + private static void sendBytes(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, byte[] bytes, String contentType, boolean encrypted) { + sendBytes(ctx, req, status, bytes, contentType, encrypted, null); + } + + private static void sendBytes(ChannelHandlerContext ctx, FullHttpRequest req, HttpResponseStatus status, byte[] bytes, String contentType, boolean encrypted, String deriveFingerprint) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes)); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-store, no-cache, must-revalidate"); + if (encrypted) response.headers().set("X-Nitro-Sec", "1"); + response.headers().set("X-Nitro-Key-Fp", SERVER_KEY_FINGERPRINT); + if (deriveFingerprint != null && !deriveFingerprint.isEmpty()) response.headers().set("X-Nitro-Derive-Fp", deriveFingerprint); + applyCors(req, response); + boolean keepAlive = isKeepAlive(req); + if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + var future = ctx.writeAndFlush(response); + if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE); + } + + private static void sendCors(ChannelHandlerContext ctx, FullHttpRequest req) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT); + applyCors(req, response); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + private static void applyCors(FullHttpRequest req, FullHttpResponse response) { + String origin = req.headers().get(HttpHeaderNames.ORIGIN); + if (origin != null && !origin.isEmpty()) { + response.headers().set("Access-Control-Allow-Origin", origin); + response.headers().set("Vary", "Origin"); + } + response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS"); + response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Nitro-Key"); + response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp"); + } + + private static boolean isKeepAlive(FullHttpRequest req) { + String connection = req.headers().get(HttpHeaderNames.CONNECTION); + return connection == null || !"close".equalsIgnoreCase(connection); + } + + static String fingerprint(byte[] bytes) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(bytes); + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < 8 && i < hash.length; i++) { + builder.append(String.format("%02x", hash[i])); + } + return builder.toString(); + } catch (Exception e) { + return "unknown"; + } + } + + static String getServerKeyFingerprint() { + return SERVER_KEY_FINGERPRINT; + } + + static String toHex(byte[] bytes) { + StringBuilder builder = new StringBuilder(bytes.length * 2); + for (byte value : bytes) { + builder.append(String.format("%02x", value & 0xff)); + } + return builder.toString(); + } + + static byte[] fromHex(String hex) { + String normalized = hex == null ? "" : hex.trim(); + if ((normalized.length() % 2) != 0) throw new IllegalArgumentException("Invalid encrypted hex payload."); + + byte[] out = new byte[normalized.length() / 2]; + for (int i = 0; i < out.length; i++) { + int high = Character.digit(normalized.charAt(i * 2), 16); + int low = Character.digit(normalized.charAt((i * 2) + 1), 16); + if (high < 0 || low < 0) throw new IllegalArgumentException("Invalid encrypted hex payload."); + out[i] = (byte) ((high << 4) | low); + } + return out; + } + + private record CacheEntry(long modified, byte[] bytes) {} +} diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/RememberJwtService.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/RememberJwtService.java index 9e2a4891..93763902 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/RememberJwtService.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/RememberJwtService.java @@ -44,7 +44,9 @@ public final class RememberJwtService { } private static int familyTtlDays() { - return Math.max(1, Emulator.getConfig().getInt("login.remember.duration.days", 30)); + int configured = Emulator.getConfig().getInt("login.remember.duration.days", 0); + if (configured <= 0) configured = Emulator.getConfig().getInt("login.remember.days", 30); + return Math.max(1, configured); } private static long familyTtlSeconds() { diff --git a/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java b/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java index 5712a3b8..ee174242 100644 --- a/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java +++ b/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java @@ -13,6 +13,7 @@ import com.eu.habbo.habbohotel.games.tag.TagGame; import com.eu.habbo.habbohotel.items.ItemManager; import com.eu.habbo.habbohotel.items.interactions.InteractionPostIt; import com.eu.habbo.habbohotel.items.interactions.InteractionRoller; +import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectSendSignal; import com.eu.habbo.habbohotel.items.interactions.games.football.InteractionFootballGate; import com.eu.habbo.habbohotel.messenger.Messenger; import com.eu.habbo.habbohotel.modtool.WordFilter; @@ -116,6 +117,7 @@ public class PluginManager { RoomManager.HOME_ROOM_ID = Emulator.getConfig().getInt("hotel.home.room"); WiredManager.MAXIMUM_FURNI_SELECTION = Emulator.getConfig().getInt("hotel.wired.furni.selection.count"); WiredManager.TELEPORT_DELAY = Emulator.getConfig().getInt("wired.effect.teleport.delay", 500); + WiredEffectSendSignal.MAX_SIGNAL_DEPTH = Emulator.getConfig().getInt("wired.signal.max.depth", 100); WiredEngine.MAX_RECURSION_DEPTH = Emulator.getConfig().getInt("wired.abuse.max.recursion.depth", 10); WiredEngine.MAX_EVENTS_PER_WINDOW = Emulator.getConfig().getInt("wired.abuse.max.events.per.window", 100); WiredEngine.RATE_LIMIT_WINDOW_MS = Emulator.getConfig().getInt("wired.abuse.rate.limit.window.ms", 10000); diff --git a/Latest_Compiled_Version/config.ini.example b/Latest_Compiled_Version/config.ini.example index 96571801..cd8bd750 100644 --- a/Latest_Compiled_Version/config.ini.example +++ b/Latest_Compiled_Version/config.ini.example @@ -10,6 +10,11 @@ db.pool.maxsize=100 # Encrypt your traffic crypto.ws.enabled=0 +# Optional packet signing for encrypted WebSocket traffic. +crypto.ws.signing.enabled=false +# Optional persistent signing keys. Leave empty to auto-generate/persist them in emulator_settings. +crypto.ws.signing.public_key= +crypto.ws.signing.private_key= #Game Configuration. #Host IP. Most likely just 0.0.0.0 Use 127.0.0.1 if you want to play on LAN. @@ -43,4 +48,24 @@ db.pool.leak_detection_ms = 20000 set to 0 to disable enc.enabled=false enc.e=3 enc.n=86851dd364d5c5cece3c883171cc6ddc5760779b992482bd1e20dd296888df91b33b936a7b93f06d29e8870f703a216257dec7c81de0058fea4cc5116f75e6efc4e9113513e45357dc3fd43d4efab5963ef178b78bd61e81a14c603b24c8bcce0a12230b320045498edc29282ff0603bc7b7dae8fc1b05b52b2f301a9dc783b7 -enc.d=59ae13e243392e89ded305764bdd9e92e4eafa67bb6dac7e1415e8c645b0950bccd26246fd0d4af37145af5fa026c0ec3a94853013eaae5ff1888360f4f9449ee023762ec195dff3f30ca0b08b8c947e3859877b5d7dced5c8715c58b53740b84e11fbc71349a27c31745fcefeeea57cff291099205e230e0c7c27e8e1c0512b \ No newline at end of file +enc.d=59ae13e243392e89ded305764bdd9e92e4eafa67bb6dac7e1415e8c645b0950bccd26246fd0d4af37145af5fa026c0ec3a94853013eaae5ff1888360f4f9449ee023762ec195dff3f30ca0b08b8c947e3859877b5d7dced5c8715c58b53740b84e11fbc71349a27c31745fcefeeea57cff291099205e230e0c7c27e8e1c0512b + +# Nitro secure runtime assets. JSON files are read live from disk. +nitro.secure.assets.enabled=true +nitro.secure.api.enabled=true +# Secure runtime ECDH session TTL in seconds. +nitro.secure.session_ttl_sec=900 +# Point this to your deployed Nitro `/configuration` folder when secure config assets are enabled. +nitro.secure.config.root= +nitro.secure.gamedata.root= +# Set a persistent secret when using Cloudflare / multiple backend requests. +nitro.secure.master_key=change-me-to-a-long-random-secret + +# Remember-me login tokens. +login.remember.enabled=true +login.remember.duration.days=30 +# Optional: set a persistent remember-me JWT secret here, otherwise one is generated and stored in emulator_settings. +login.remember.jwt.secret= + +# Login news API. +login.news.limit=5