Merge pull request #99 from Lorenzune/merge-duckie-main-2026-05-06

Merge live secure runtime updates into dev
This commit is contained in:
DuckieTM
2026-05-06 07:08:37 +02:00
committed by GitHub
87 changed files with 3898 additions and 291 deletions
+2 -3
View File
@@ -91,7 +91,7 @@ ALTER TABLE `catalog_pages`
'builders_club_addons', 'builders_club_addons',
'builders_club_loyalty' 'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3'; ) NOT NULL DEFAULT 'default_3x3';
ALTER TABLE `catalog_pages` ALTER TABLE `catalog_pages`
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL','BUILDER','BOTH') NOT NULL DEFAULT 'NORMAL' ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL','BUILDER','BOTH') NOT NULL DEFAULT 'NORMAL'
AFTER `club_only`; AFTER `club_only`;
@@ -878,7 +878,7 @@ ON DUPLICATE KEY UPDATE
`permission` = VALUES(`permission`), `permission` = VALUES(`permission`),
`overridable` = VALUES(`overridable`), `overridable` = VALUES(`overridable`),
`triggers_talking_furniture` = VALUES(`triggers_talking_furniture`); `triggers_talking_furniture` = VALUES(`triggers_talking_furniture`);
ALTER TABLE `catalog_club_offers` ALTER TABLE `catalog_club_offers`
MODIFY COLUMN `type` ENUM('HC', 'VIP', 'BUILDERS_CLUB', 'BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC'; 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_addons',
'builders_club_loyalty' 'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3'; ) NOT NULL DEFAULT 'default_3x3';
+286 -12
View File
@@ -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` ( CREATE TABLE IF NOT EXISTS `user_prefixes` (
`id` INT(11) NOT NULL AUTO_INCREMENT, `id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL, `user_id` INT(11) NOT NULL,
@@ -5,28 +15,57 @@ CREATE TABLE IF NOT EXISTS `user_prefixes` (
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF', `color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
`icon` VARCHAR(50) NOT NULL DEFAULT '', `icon` VARCHAR(50) NOT NULL DEFAULT '',
`effect` 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, `active` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
INDEX `idx_user_id` (`user_id`), INDEX `idx_user_id` (`user_id`),
INDEX `idx_user_active` (`user_id`, `active`) INDEX `idx_user_active` (`user_id`, `active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) 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` ( CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
`key_name` VARCHAR(100) NOT NULL, `key_name` VARCHAR(100) NOT NULL,
`value` VARCHAR(255) NOT NULL, `value` VARCHAR(255) NOT NULL,
PRIMARY KEY (`key_name`) PRIMARY KEY (`key_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Default settings -- ------------------------------------------------------------
INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES -- 5. Blacklist table
('max_length', '15'), -- ------------------------------------------------------------
('min_rank_to_buy', '1'),
('price_credits', '5'),
('price_points', '0'),
('points_type', '0');
-- 3. Blacklisted words table
CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` ( CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
`id` INT(11) NOT NULL AUTO_INCREMENT, `id` INT(11) NOT NULL AUTO_INCREMENT,
`word` VARCHAR(100) NOT NULL, `word` VARCHAR(100) NOT NULL,
@@ -34,13 +73,249 @@ CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
UNIQUE KEY `uk_word` (`word`) UNIQUE KEY `uk_word` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) 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 INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES
('admin'), ('admin'),
('staff'), ('staff'),
('mod'), ('mod'),
('owner'); ('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 INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
-- GivePrefix command -- GivePrefix command
('commands.keys.cmd_give_prefix', 'giveprefix'), ('commands.keys.cmd_give_prefix', 'giveprefix'),
@@ -79,4 +354,3 @@ VALUES
('cmd_list_prefixes', '1', '0', '0', '0', '0', '0', '0', '1'), ('cmd_list_prefixes', '1', '0', '0', '0', '0', '0', '0', '1'),
('cmd_remove_prefix', '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'); ('cmd_prefix_blacklist', '1', '0', '0', '0', '0', '0', '0', '1');
@@ -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`;
@@ -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`);
@@ -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';
+3 -3
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId> <groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId> <artifactId>Habbo</artifactId>
<version>4.1.2</version> <version>4.1.13</version>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -163,7 +163,7 @@
<version>2.13.0</version> <version>2.13.0</version>
</dependency> </dependency>
<!-- jBCrypt — used by the built-in /api/auth/* HTTP login handler <!-- jBCrypt — used by the built-in /api/auth/* HTTP login handler
to verify Laravel-style $2y$ BCrypt hashes from users.password --> to verify Laravel-style $2y$ BCrypt hashes from users.password -->
<dependency> <dependency>
<groupId>org.mindrot</groupId> <groupId>org.mindrot</groupId>
@@ -171,7 +171,7 @@
<version>0.4</version> <version>0.4</version>
</dependency> </dependency>
<!-- Jakarta Mail — used by the built-in forgot-password endpoint <!-- Jakarta Mail — used by the built-in forgot-password endpoint
when smtp.* keys are configured in emulator_settings --> when smtp.* keys are configured in emulator_settings -->
<dependency> <dependency>
<groupId>org.eclipse.angus</groupId> <groupId>org.eclipse.angus</groupId>
@@ -21,6 +21,7 @@ import com.eu.habbo.habbohotel.pets.PetManager;
import com.eu.habbo.habbohotel.polls.PollManager; import com.eu.habbo.habbohotel.polls.PollManager;
import com.eu.habbo.habbohotel.rooms.RoomChatBubbleManager; import com.eu.habbo.habbohotel.rooms.RoomChatBubbleManager;
import com.eu.habbo.habbohotel.rooms.RoomManager; 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.HabboManager;
import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeManager; import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeManager;
import com.eu.habbo.habbohotel.users.infostand.InfostandBackgroundManager; import com.eu.habbo.habbohotel.users.infostand.InfostandBackgroundManager;
@@ -60,6 +61,7 @@ public class GameEnvironment {
private SubscriptionManager subscriptionManager; private SubscriptionManager subscriptionManager;
private CalendarManager calendarManager; private CalendarManager calendarManager;
private RoomChatBubbleManager roomChatBubbleManager; private RoomChatBubbleManager roomChatBubbleManager;
private GoogleTranslateManager googleTranslateManager;
private CustomBadgeManager customBadgeManager; private CustomBadgeManager customBadgeManager;
private InfostandBackgroundManager infostandBackgroundManager; private InfostandBackgroundManager infostandBackgroundManager;
@@ -88,6 +90,7 @@ public class GameEnvironment {
this.pollManager = new PollManager(); this.pollManager = new PollManager();
this.calendarManager = new CalendarManager(); this.calendarManager = new CalendarManager();
this.roomChatBubbleManager = new RoomChatBubbleManager(); this.roomChatBubbleManager = new RoomChatBubbleManager();
this.googleTranslateManager = new GoogleTranslateManager();
this.customBadgeManager = new CustomBadgeManager(); this.customBadgeManager = new CustomBadgeManager();
this.infostandBackgroundManager = new InfostandBackgroundManager(); this.infostandBackgroundManager = new InfostandBackgroundManager();
@@ -127,6 +130,9 @@ public class GameEnvironment {
this.hotelViewManager.dispose(); this.hotelViewManager.dispose();
this.subscriptionManager.dispose(); this.subscriptionManager.dispose();
this.calendarManager.dispose(); this.calendarManager.dispose();
if (this.googleTranslateManager != null) {
this.googleTranslateManager.clearCache();
}
LOGGER.info("GameEnvironment -> Disposed!"); LOGGER.info("GameEnvironment -> Disposed!");
} }
@@ -226,6 +232,10 @@ public class GameEnvironment {
return roomChatBubbleManager; return roomChatBubbleManager;
} }
public GoogleTranslateManager getGoogleTranslateManager() {
return this.googleTranslateManager;
}
public CustomBadgeManager getCustomBadgeManager() { public CustomBadgeManager getCustomBadgeManager() {
return this.customBadgeManager; return this.customBadgeManager;
} }
@@ -1,7 +1,6 @@
package com.eu.habbo.habbohotel.achievements; package com.eu.habbo.habbohotel.achievements;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.database.SqlQueries;
import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboBadge; import com.eu.habbo.habbohotel.users.HabboBadge;
@@ -50,12 +49,16 @@ public class AchievementManager {
if (habbo != null) { if (habbo != null) {
progressAchievement(habbo, achievement, amount); progressAchievement(habbo, achievement, amount);
} else { } else {
try { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
SqlQueries.update( PreparedStatement statement = connection.prepareStatement("" +
"INSERT INTO users_achievements_queue (user_id, achievement_id, amount) VALUES (?, ?, ?) " "INSERT INTO users_achievements_queue (user_id, achievement_id, amount) VALUES (?, ?, ?) " +
+ "ON DUPLICATE KEY UPDATE amount = amount + ?", "ON DUPLICATE KEY UPDATE amount = amount + ?")) {
habboId, achievement.id, amount, amount); statement.setInt(1, habboId);
} catch (SqlQueries.DataAccessException e) { statement.setInt(2, achievement.id);
statement.setInt(3, amount);
statement.setInt(4, amount);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
} }
} }
@@ -200,41 +203,48 @@ public class AchievementManager {
} }
public static void createUserEntry(Habbo habbo, Achievement achievement) { public static void createUserEntry(Habbo habbo, Achievement achievement) {
try { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_achievements (user_id, achievement_name, progress) VALUES (?, ?, ?)")) {
SqlQueries.update( statement.setInt(1, habbo.getHabboInfo().getId());
"INSERT INTO users_achievements (user_id, achievement_name, progress) VALUES (?, ?, ?)", statement.setString(2, achievement.name);
habbo.getHabboInfo().getId(), achievement.name, 1); statement.setInt(3, 1);
} catch (SqlQueries.DataAccessException e) { statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
} }
} }
public static void saveAchievements(Habbo habbo) { public static void saveAchievements(Habbo habbo) {
int userId = habbo.getHabboInfo().getId(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users_achievements SET progress = ? WHERE achievement_name = ? AND user_id = ? LIMIT 1")) {
try { statement.setInt(3, habbo.getHabboInfo().getId());
SqlQueries.batchUpdate( for (Map.Entry<Achievement, Integer> map : habbo.getHabboStats().getAchievementProgress().entrySet()) {
"UPDATE users_achievements SET progress = ? WHERE achievement_name = ? AND user_id = ? LIMIT 1", statement.setInt(1, map.getValue());
habbo.getHabboStats().getAchievementProgress().entrySet(), statement.setString(2, map.getKey().name);
(ps, entry) -> { statement.addBatch();
ps.setInt(1, entry.getValue()); }
ps.setString(2, entry.getKey().name); statement.executeBatch();
ps.setInt(3, userId); } catch (SQLException e) {
});
} catch (SqlQueries.DataAccessException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
} }
} }
public static int getAchievementProgressForHabbo(int userId, Achievement achievement) { public static int getAchievementProgressForHabbo(int userId, Achievement achievement) {
try { if (achievement == null) {
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);
return 0; 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() { public void reload() {
@@ -71,13 +71,23 @@ public class BotManager {
} }
public Bot createBot(THashMap<String, String> data, String type) { public Bot createBot(THashMap<String, String> data, String type) {
return this.createBot(data, type, 0);
}
public Bot createBot(THashMap<String, String> 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; 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)) { 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.setString(1, data.get("name")); statement.setInt(1, ownerId);
statement.setString(2, data.get("motto")); statement.setString(2, data.get("name"));
statement.setString(3, data.get("figure")); statement.setString(3, data.get("motto"));
statement.setString(4, data.get("gender").toUpperCase()); statement.setString(4, data.get("figure"));
statement.setString(5, type); statement.setString(5, data.get("gender").toUpperCase());
statement.setString(6, type);
statement.execute(); statement.execute();
try (ResultSet set = statement.getGeneratedKeys()) { try (ResultSet set = statement.getGeneratedKeys()) {
if (set.next()) { if (set.next()) {
@@ -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) { if (bot != null) {
bot.setOwnerId(habbo.getClient().getHabbo().getHabboInfo().getId()); bot.setOwnerId(habbo.getClient().getHabbo().getHabboInfo().getId());
@@ -1,5 +1,6 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions; 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.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; 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.wired.core.WiredSourceUtil;
import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.ServerMessage;
import gnu.trove.set.hash.THashSet;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
public class WiredConditionSelectionQuantity extends InteractionWiredCondition { public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private static final int COMPARISON_LESS_THAN = 0; 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_USERS = 0;
private static final int SOURCE_GROUP_FURNI = 1; 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; public static final WiredConditionType type = WiredConditionType.SLC_QUANTITY;
private final THashSet<HabboItem> items;
private int comparison = COMPARISON_EQUAL; private int comparison = COMPARISON_EQUAL;
private int quantity = 0; private int quantity = 0;
private int sourceGroup = SOURCE_GROUP_USERS; private int sourceGroup = SOURCE_GROUP_USERS;
@@ -33,10 +43,12 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
public WiredConditionSelectionQuantity(ResultSet set, Item baseItem) throws SQLException { public WiredConditionSelectionQuantity(ResultSet set, Item baseItem) throws SQLException {
super(set, baseItem); super(set, baseItem);
this.items = new THashSet<>();
} }
public WiredConditionSelectionQuantity(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { public WiredConditionSelectionQuantity(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
super(id, userId, item, extradata, limitedStack, limitedSells); super(id, userId, item, extradata, limitedStack, limitedSells);
this.items = new THashSet<>();
} }
@Override @Override
@@ -46,9 +58,18 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
@Override @Override
public void serializeWiredData(ServerMessage message, Room room) { public void serializeWiredData(ServerMessage message, Room room) {
message.appendBoolean(false); this.refresh(room);
message.appendInt(5);
message.appendInt(0); 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.getBaseItem().getSpriteId());
message.appendInt(this.getId()); message.appendInt(this.getId());
message.appendString(""); message.appendString("");
@@ -69,8 +90,36 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL; this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL;
this.quantity = (params.length > 1) ? this.normalizeQuantity(params[1]) : 0; this.quantity = (params.length > 1) ? this.normalizeQuantity(params[1]) : 0;
this.sourceGroup = (params.length > 2) ? this.normalizeSourceGroup(params[2]) : SOURCE_GROUP_USERS; this.items.clear();
this.sourceType = (params.length > 3) ? this.normalizeSourceType(this.sourceGroup, params[3]) : WiredSourceUtil.SOURCE_TRIGGER;
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; return true;
} }
@@ -97,11 +146,14 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
@Override @Override
public String getWiredData() { public String getWiredData() {
this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()));
return WiredManager.getGson().toJson(new JsonData( return WiredManager.getGson().toJson(new JsonData(
this.comparison, this.comparison,
this.quantity, this.quantity,
this.sourceGroup, 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.quantity = this.normalizeQuantity(data.quantity);
this.sourceGroup = this.normalizeSourceGroup(data.sourceGroup); this.sourceGroup = this.normalizeSourceGroup(data.sourceGroup);
this.sourceType = this.normalizeSourceType(this.sourceGroup, data.sourceType); this.sourceType = this.normalizeSourceType(this.sourceGroup, data.sourceType);
this.loadSelectedItems(data.itemIds, room);
return; return;
} }
@@ -150,6 +203,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
@Override @Override
public void onPickUp() { public void onPickUp() {
this.items.clear();
this.comparison = COMPARISON_EQUAL; this.comparison = COMPARISON_EQUAL;
this.quantity = 0; this.quantity = 0;
this.sourceGroup = SOURCE_GROUP_USERS; this.sourceGroup = SOURCE_GROUP_USERS;
@@ -158,7 +212,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private int resolveCount(WiredContext ctx) { private int resolveCount(WiredContext ctx) {
if (this.sourceGroup == SOURCE_GROUP_FURNI) { if (this.sourceGroup == SOURCE_GROUP_FURNI) {
List<HabboItem> items = WiredSourceUtil.resolveItems(ctx, this.sourceType, null); List<HabboItem> items = WiredSourceUtil.resolveItems(ctx, this.sourceType, this.items);
return items.size(); return items.size();
} }
@@ -188,10 +242,18 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private int normalizeSourceType(int group, int value) { private int normalizeSourceType(int group, int value) {
if (group == SOURCE_GROUP_USERS) { 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) { switch (value) {
case WiredSourceUtil.SOURCE_SELECTED:
case WiredSourceUtil.SOURCE_SELECTOR: case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL: case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER: 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<Integer> 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<HabboItem> 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 { static class JsonData {
int comparison; int comparison;
int quantity; int quantity;
int sourceGroup; int sourceGroup;
int sourceType; int sourceType;
List<Integer> itemIds;
public JsonData(int comparison, int quantity, int sourceGroup, int sourceType) { public JsonData(int comparison, int quantity, int sourceGroup, int sourceType, List<Integer> itemIds) {
this.comparison = comparison; this.comparison = comparison;
this.quantity = quantity; this.quantity = quantity;
this.sourceGroup = sourceGroup; this.sourceGroup = sourceGroup;
this.sourceType = sourceType; this.sourceType = sourceType;
this.itemIds = itemIds;
} }
} }
} }
@@ -82,7 +82,7 @@ public class WiredEffectBotTalk extends InteractionWiredEffect {
this.setDelay(delay); this.setDelay(delay);
this.botName = data[0].substring(0, Math.min(data[0].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100))); 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.mode = mode;
return true; return true;
@@ -105,7 +105,7 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect {
throw new WiredSaveException("Delay too long"); 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.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.mode = mode;
this.setDelay(delay); this.setDelay(delay);
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; 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.games.InteractionGameUpCounter;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.Room;
@@ -60,29 +61,74 @@ public class WiredEffectControlClock extends InteractionWiredEffect {
} }
for (HabboItem item : effectiveItems) { for (HabboItem item : effectiveItems) {
if (!(item instanceof InteractionGameUpCounter)) { if (!(item instanceof InteractionGameTimer)) {
continue; continue;
} }
InteractionGameUpCounter counter = (InteractionGameUpCounter) item; if (item instanceof InteractionGameUpCounter) {
this.controlUpCounter((InteractionGameUpCounter) item, room);
switch (this.action) { continue;
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;
} }
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)); 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"); throw new WiredSaveException("wiredfurni.error.require_counter_furni");
} }
@@ -53,26 +53,37 @@ public class WiredEffectFurniToFurni extends InteractionWiredEffect {
return; return;
} }
HabboItem moveItem = this.resolveLastMoveItem(ctx); List<HabboItem> moveItems = this.resolveMoveItems(ctx);
HabboItem targetItem = this.resolveLastTargetItem(ctx); List<HabboItem> targetItems = this.resolveTargetItems(ctx);
if (moveItem == null || targetItem == null || moveItem.getId() == targetItem.getId()) { if (moveItems.isEmpty() || targetItems.isEmpty()) {
return; return;
} }
RoomTile targetTile = room.getLayout().getTile(targetItem.getX(), targetItem.getY()); int targetIndex = 0;
if (targetTile == null) { for (HabboItem moveItem : moveItems) {
return; if (moveItem == null) {
} continue;
}
FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), null, false, ctx); HabboItem targetItem = targetItems.get(targetIndex % targetItems.size());
if (error == FurnitureMovementError.NONE) { targetIndex++;
return;
}
error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), targetItem.getZ(), null, false, ctx); if (targetItem == null || moveItem.getId() == targetItem.getId()) {
if (error == FurnitureMovementError.NONE) { continue;
return; }
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; return COOLDOWN_MOVEMENT;
} }
private HabboItem resolveLastMoveItem(WiredContext ctx) { private List<HabboItem> resolveMoveItems(WiredContext ctx) {
return this.resolveLastItem(ctx, this.moveSource, this.moveItems); return this.resolveItems(ctx, this.moveSource, this.moveItems);
} }
private HabboItem resolveLastTargetItem(WiredContext ctx) { private List<HabboItem> resolveTargetItems(WiredContext ctx) {
int source = (this.targetSource == SOURCE_SECONDARY_SELECTED) ? WiredSourceUtil.SOURCE_SELECTED : this.targetSource; 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<HabboItem> items) { private List<HabboItem> resolveItems(WiredContext ctx, int source, List<HabboItem> items) {
if (source == WiredSourceUtil.SOURCE_SELECTED) { if (source == WiredSourceUtil.SOURCE_SELECTED) {
this.validateItems(items); this.validateItems(items);
} }
List<HabboItem> resolvedItems = WiredSourceUtil.resolveItems(ctx, source, items); return WiredSourceUtil.resolveItems(ctx, source, items).stream()
.filter(item -> item != null && ctx.room().getHabboItem(item.getId()) != null)
if (resolvedItems.isEmpty()) { .collect(Collectors.toList());
return null;
}
for (int index = resolvedItems.size() - 1; index >= 0; index--) {
HabboItem item = resolvedItems.get(index);
if (item != null) {
return item;
}
}
return null;
} }
private List<HabboItem> parseItems(String data, Room room) throws WiredSaveException { private List<HabboItem> parseItems(String data, Room room) throws WiredSaveException {
@@ -33,7 +33,7 @@ public class WiredEffectSendSignal extends InteractionWiredEffect {
public static final WiredEffectType type = WiredEffectType.SEND_SIGNAL; 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_PICKED = 0;
private static final int ANTENNA_TRIGGER = 1; private static final int ANTENNA_TRIGGER = 1;
@@ -166,7 +166,7 @@ public class WiredEffectSendSignal extends InteractionWiredEffect {
.signalChannel(signalChannel) .signalChannel(signalChannel)
.signalUserCount(signalUserCount) .signalUserCount(signalUserCount)
.signalFurniCount(sourceItem != null ? 1 : 0) .signalFurniCount(sourceItem != null ? 1 : 0)
.contextVariableScope(ctx.contextVariables()) .contextVariableScope(ctx.contextVariables().copy())
.triggeredByEffect(true); .triggeredByEffect(true);
if (actor != null) builder.actor(actor); 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(); int delay = settings.getDelay();
if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) {
throw new WiredSaveException("Delay too long"); throw new WiredSaveException("Delay too long");
@@ -34,6 +34,8 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
private static final long DELIVERY_DEDUP_TTL_MS = 60_000L; private static final long DELIVERY_DEDUP_TTL_MS = 60_000L;
private static final int DELIVERY_DEDUP_CLEANUP_THRESHOLD = 512; private static final int DELIVERY_DEDUP_CLEANUP_THRESHOLD = 512;
private static final ConcurrentHashMap<String, Long> DELIVERY_DEDUP = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<String, Long> 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 String message = "";
protected int userSource = WiredSourceUtil.SOURCE_TRIGGER; 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)) { if(gameClient.getHabbo() == null || !gameClient.getHabbo().hasPermission(Permission.ACC_SUPERWIRED)) {
message = Emulator.getGameEnvironment().getWordFilter().filter(message, null); 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(); int delay = settings.getDelay();
if(delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) if(delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20))
@@ -109,6 +114,35 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
return true; 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<RoomUnit> resolveUsers(WiredContext ctx) { protected List<RoomUnit> resolveUsers(WiredContext ctx) {
return WiredSourceUtil.resolveUsers(ctx, this.userSource); return WiredSourceUtil.resolveUsers(ctx, this.userSource);
} }
@@ -212,7 +246,9 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
} }
String msg = buildMessage(ctx, (sharedSourceHabbo != null) ? sharedSourceHabbo : habbo); 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()) { if (habbo.getRoomUnit().isIdle()) {
habbo.getRoomUnit().getRoom().unIdle(habbo); habbo.getRoomUnit().getRoom().unIdle(habbo);
@@ -170,13 +170,15 @@ public class WiredExtraTextInputVariable extends InteractionWiredExtra {
} }
public Integer resolveCapturedValue(Room room, String rawValue) { public Integer resolveCapturedValue(Room room, String rawValue) {
String normalizedValue = rawValue != null ? rawValue.trim() : ""; String capturedValue = rawValue != null ? rawValue : "";
if (normalizedValue.isEmpty()) { String normalizedValue = capturedValue.trim();
return null;
}
if (this.getDisplayType(room) == DISPLAY_TEXTUAL) { 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 { try {
@@ -22,6 +22,7 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
public static final int CODE = 79; public static final int CODE = 79;
public static final int MAX_MAPPING_LENGTH = 1000; public static final int MAX_MAPPING_LENGTH = 1000;
public static final int MAX_MAPPING_LINES = 30; public static final int MAX_MAPPING_LINES = 30;
private static final String PRESERVED_SPACE = "\u00A0";
private String mappingsText = ""; private String mappingsText = "";
private LinkedHashMap<Integer, String> mappings = new LinkedHashMap<>(); private LinkedHashMap<Integer, String> mappings = new LinkedHashMap<>();
@@ -123,8 +124,12 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
return ""; return "";
} }
String mappedValue = this.mappings.get(value); if (this.mappings.containsKey(value)) {
return mappedValue != null ? mappedValue : String.valueOf(value); String mappedValue = this.mappings.get(value);
return mappedValue != null ? preserveSpaces(mappedValue) : "";
}
return String.valueOf(value);
} }
public Integer resolveValue(String text) { public Integer resolveValue(String text) {
@@ -132,17 +137,16 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
return null; return null;
} }
String normalizedText = text.trim(); String normalizedText = normalizePreservedSpaces(text);
if (normalizedText.isEmpty()) {
return null;
}
for (Map.Entry<Integer, String> entry : this.mappings.entrySet()) { for (Map.Entry<Integer, String> entry : this.mappings.entrySet()) {
if (entry == null || entry.getKey() == null || entry.getValue() == null) { if (entry == null || entry.getKey() == null || entry.getValue() == null) {
continue; continue;
} }
if (entry.getValue().trim().equalsIgnoreCase(normalizedText)) { String normalizedMappingValue = normalizePreservedSpaces(entry.getValue());
if (normalizedMappingValue.equalsIgnoreCase(normalizedText)) {
return entry.getKey(); return entry.getKey();
} }
} }
@@ -195,8 +199,8 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
continue; continue;
} }
String line = rawLine.trim(); String line = rawLine;
if (line.isEmpty()) { if (line.trim().isEmpty()) {
continue; continue;
} }
@@ -210,7 +214,7 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
} }
String keyPart = line.substring(0, separatorIndex).trim(); String keyPart = line.substring(0, separatorIndex).trim();
String valuePart = line.substring(separatorIndex + 1).trim(); String valuePart = line.substring(separatorIndex + 1);
try { try {
result.put(Integer.parseInt(keyPart), valuePart); result.put(Integer.parseInt(keyPart), valuePart);
@@ -221,6 +225,14 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
return result; 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 { static class JsonData {
String mappingsText; String mappingsText;
@@ -95,6 +95,11 @@ public class WiredEffectFurniAltitude extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData( return WiredManager.getGson().toJson(new JsonData(
@@ -100,6 +100,11 @@ public class WiredEffectFurniArea extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay())); return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay()));
@@ -155,6 +155,11 @@ public class WiredEffectFurniByType extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson( return WiredManager.getGson().toJson(
@@ -38,6 +38,7 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
private static final int MAX_PICKED_FURNI = 20; private static final int MAX_PICKED_FURNI = 20;
private static final int MAX_TILE_OFFSETS = 64; private static final int MAX_TILE_OFFSETS = 64;
private static final int GRID_RANGE = 4;
private int sourceType = SOURCE_USER_TRIGGER; private int sourceType = SOURCE_USER_TRIGGER;
private boolean filterExisting = false; private boolean filterExisting = false;
@@ -69,8 +70,20 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
int totalRaw = 0; int totalRaw = 0;
int wiredSkipped = 0; int wiredSkipped = 0;
Set<HabboItem> result = new LinkedHashSet<>(); Set<HabboItem> result = new LinkedHashSet<>();
Set<HabboItem> neighborhoodItems = new LinkedHashSet<>();
for (int[] src : sourcePositions) { for (int[] src : sourcePositions) {
LOGGER.info("[FurniNeighborhood] Source: ({},{}), offsets: {}", src[0], src[1], tileOffsets.size()); 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) { for (int[] offset : tileOffsets) {
int tx = src[0] + (offset[0] - this.targetOffsetX); int tx = src[0] + (offset[0] - this.targetOffsetX);
int ty = src[1] + (offset[1] - this.targetOffsetY); 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()); 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. // Always set the selector result even if empty.
// An empty result means no items matched the neighborhood, so downstream // 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()); LOGGER.info("[FurniNeighborhood] Set {} items as targets", result.size());
} }
private List<int[]> getFullGridOffsets() {
List<int[]> 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<HabboItem> applyNeighborhoodModifiers(Set<HabboItem> matchedTargets,
Set<HabboItem> neighborhoodTargets,
Collection<HabboItem> existingTargets) {
LinkedHashSet<HabboItem> matched = new LinkedHashSet<>(matchedTargets);
if (this.invert) {
LinkedHashSet<HabboItem> 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<int[]> resolveSourcePositions(WiredContext ctx, Room room) { private List<int[]> resolveSourcePositions(WiredContext ctx, Room room) {
switch (sourceType) { switch (sourceType) {
case SOURCE_USER_TRIGGER: { case SOURCE_USER_TRIGGER: {
if (ctx.tile().isPresent()) { Optional<RoomUnit> actor = ctx.actor();
return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y }); if (actor.isPresent()) {
return Collections.singletonList(new int[]{ actor.get().getX(), actor.get().getY() });
} }
return ctx.actor() return ctx.tile()
.map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() })) .map(tile -> Collections.singletonList(new int[]{ tile.x, tile.y }))
.orElse(Collections.emptyList()); .orElse(Collections.emptyList());
} }
case SOURCE_USER_SIGNAL: { case SOURCE_USER_SIGNAL: {
@@ -260,6 +309,16 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean hasRequiredSelectorTargets(WiredContext ctx) {
return ctx != null && ctx.targets().hasItems();
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson( return WiredManager.getGson().toJson(
@@ -128,6 +128,11 @@ public class WiredEffectFurniOnFurni extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId())); this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()));
@@ -86,6 +86,11 @@ public class WiredEffectFurniPicks extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData( return WiredManager.getGson().toJson(new JsonData(
@@ -77,6 +77,11 @@ public class WiredEffectFurniSignal extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay())); return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay()));
@@ -86,6 +86,11 @@ public class WiredEffectUsersArea extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay())); return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay()));
@@ -92,6 +92,11 @@ public class WiredEffectUsersByAction extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData( return WiredManager.getGson().toJson(new JsonData(
@@ -90,6 +90,11 @@ public class WiredEffectUsersByName extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.namesText, this.filterExisting, this.invert, this.getDelay())); return WiredManager.getGson().toJson(new JsonData(this.namesText, this.filterExisting, this.invert, this.getDelay()));
@@ -76,6 +76,11 @@ public class WiredEffectUsersByType extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.entityType, this.filterExisting, this.invert, this.getDelay())); return WiredManager.getGson().toJson(new JsonData(this.entityType, this.filterExisting, this.invert, this.getDelay()));
@@ -90,6 +90,11 @@ public class WiredEffectUsersGroup extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.groupType, this.selectedGroupId, this.filterExisting, this.invert, this.getDelay())); return WiredManager.getGson().toJson(new JsonData(this.groupType, this.selectedGroupId, this.filterExisting, this.invert, this.getDelay()));
@@ -73,6 +73,11 @@ public class WiredEffectUsersHandItem extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.handItemId, this.filterExisting, this.invert, this.getDelay())); return WiredManager.getGson().toJson(new JsonData(this.handItemId, this.filterExisting, this.invert, this.getDelay()));
@@ -38,6 +38,7 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
private static final int MAX_PICKED_FURNI = 20; private static final int MAX_PICKED_FURNI = 20;
private static final int MAX_TILE_OFFSETS = 64; private static final int MAX_TILE_OFFSETS = 64;
private static final int GRID_RANGE = 4;
private int sourceType = SOURCE_USER_TRIGGER; private int sourceType = SOURCE_USER_TRIGGER;
private boolean filterExisting = false; private boolean filterExisting = false;
@@ -87,11 +88,25 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
LOGGER.debug("[Neighborhood] Target tiles: {}", targetTiles); LOGGER.debug("[Neighborhood] Target tiles: {}", targetTiles);
Set<String> 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<RoomUnit> result = new ArrayList<>(); List<RoomUnit> result = new ArrayList<>();
List<RoomUnit> neighborhoodUsers = new ArrayList<>();
for (RoomUnit unit : room.getRoomUnits()) { for (RoomUnit unit : room.getRoomUnits()) {
String pos = unit.getX() + "," + unit.getY(); String pos = unit.getX() + "," + unit.getY();
boolean onTile = targetTiles.contains(pos); 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); LOGGER.debug("[Neighborhood] Unit id={} type={} pos={} onTile={}", unit.getId(), unit.getRoomUnitType(), pos, onTile);
if (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()); LOGGER.debug("[Neighborhood] Result: {} users selected", result.size());
@@ -110,15 +125,51 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
ctx.targets().setUsers(result); ctx.targets().setUsers(result);
} }
private List<int[]> getFullGridOffsets() {
List<int[]> 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<RoomUnit> applyNeighborhoodModifiers(Collection<RoomUnit> matchedTargets,
Collection<RoomUnit> neighborhoodTargets,
Collection<RoomUnit> existingTargets) {
LinkedHashSet<RoomUnit> matched = new LinkedHashSet<>(matchedTargets);
if (this.invert) {
LinkedHashSet<RoomUnit> 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<int[]> resolveSourcePositions(WiredContext ctx, Room room) { private List<int[]> resolveSourcePositions(WiredContext ctx, Room room) {
switch (sourceType) { switch (sourceType) {
case SOURCE_USER_TRIGGER: { case SOURCE_USER_TRIGGER: {
if (ctx.tile().isPresent()) { Optional<RoomUnit> actor = ctx.actor();
return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y }); if (actor.isPresent()) {
return Collections.singletonList(new int[]{ actor.get().getX(), actor.get().getY() });
} }
return ctx.actor() return ctx.tile()
.map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() })) .map(tile -> Collections.singletonList(new int[]{ tile.x, tile.y }))
.orElse(Collections.emptyList()); .orElse(Collections.emptyList());
} }
case SOURCE_USER_SIGNAL: { case SOURCE_USER_SIGNAL: {
@@ -262,6 +313,16 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean hasRequiredSelectorTargets(WiredContext ctx) {
return ctx != null && ctx.targets().hasUsers();
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson( return WiredManager.getGson().toJson(
@@ -115,6 +115,11 @@ public class WiredEffectUsersOnFurni extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId())); this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()));
@@ -71,6 +71,11 @@ public class WiredEffectUsersSignal extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay())); return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay()));
@@ -76,6 +76,11 @@ public class WiredEffectUsersTeam extends InteractionWiredEffect {
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.teamType, this.filterExisting, this.invert, this.getDelay())); return WiredManager.getGson().toJson(new JsonData(this.teamType, this.filterExisting, this.invert, this.getDelay()));
@@ -187,6 +187,11 @@ public abstract class WiredEffectVariableSelectorBase extends InteractionWiredEf
return true; return true;
} }
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override @Override
public String getWiredData() { public String getWiredData() {
this.refreshReferenceItems(); this.refreshReferenceItems();
@@ -370,8 +370,14 @@ public class PetManager {
} else { } else {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
LOGGER.error("Missing petdata for type {}. Adding this to the database...", type); 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.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(); statement.execute();
} }
@@ -411,6 +417,42 @@ public class PetManager {
return this.petData.values(); 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) { public Pet createPet(Item item, String name, String race, String color, GameClient client) {
int type = Integer.parseInt(item.getName().toLowerCase().replace("a0 pet", "")); int type = Integer.parseInt(item.getName().toLowerCase().replace("a0 pet", ""));
@@ -540,4 +582,4 @@ public class PetManager {
return false; return false;
} }
} }
@@ -1177,7 +1177,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
if (this.needsUpdate) { if (this.needsUpdate) {
try (Connection connection = Emulator.getDatabase().getDataSource() try (Connection connection = Emulator.getDatabase().getDataSource()
.getConnection(); PreparedStatement statement = connection.prepareStatement( .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(1, this.name);
statement.setString(2, this.description); statement.setString(2, this.description);
statement.setString(3, this.password); statement.setString(3, this.password);
@@ -1228,7 +1228,9 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
statement.setString(39, this.hideWired ? "1" : "0"); statement.setString(39, this.hideWired ? "1" : "0");
statement.setString(40, this.allowUnderpass ? "1" : "0"); statement.setString(40, this.allowUnderpass ? "1" : "0");
statement.setString(41, this.youtubeEnabled ? "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(); statement.executeUpdate();
this.needsUpdate = false; this.needsUpdate = false;
} catch (SQLException e) { } catch (SQLException e) {
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.core.DatabaseLoggable; import com.eu.habbo.core.DatabaseLoggable;
import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo; 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.ISerialize;
import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.incoming.Incoming; import com.eu.habbo.messages.incoming.Incoming;
@@ -204,23 +205,14 @@ public class RoomChatMessage implements Runnable, ISerialize, DatabaseLoggable {
message.appendInt(this.getMessage().length()); message.appendInt(this.getMessage().length());
// Custom prefix data // Custom prefix data
String prefixText = ""; UserCustomizationData customizationData = (this.habbo != null) ? UserCustomizationData.fromHabbo(this.habbo) : UserCustomizationData.empty();
String prefixColor = ""; message.appendString(customizationData.prefixText);
String prefixIcon = ""; message.appendString(customizationData.prefixColor);
String prefixEffect = ""; message.appendString(customizationData.prefixIcon);
if (this.habbo != null && this.habbo.getInventory() != null && this.habbo.getInventory().getPrefixesComponent() != null) { message.appendString(customizationData.prefixEffect);
com.eu.habbo.habbohotel.users.UserPrefix activePrefix = this.habbo.getInventory().getPrefixesComponent().getActivePrefix(); message.appendString(customizationData.prefixFont);
if (activePrefix != null) { message.appendString(customizationData.nickIcon);
prefixText = activePrefix.getText(); message.appendString(customizationData.displayOrder);
prefixColor = activePrefix.getColor();
prefixIcon = activePrefix.getIcon();
prefixEffect = activePrefix.getEffect();
}
}
message.appendString(prefixText);
message.appendString(prefixColor);
message.appendString(prefixIcon);
message.appendString(prefixEffect);
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Caught exception", e); LOGGER.error("Caught exception", e);
} }
@@ -343,18 +343,16 @@ public class RoomSpecialTypes {
* Adds a wired trigger to the room. * Adds a wired trigger to the room.
* @param trigger The trigger to add * @param trigger The trigger to add
*/ */
public static final int MAX_SIGNAL_SENDERS_PER_ROOM = 25; public static final int MAX_SIGNAL_SENDERS_PER_ROOM = 0;
public static final int MAX_SIGNAL_RECEIVERS_PER_ROOM = 5; public static final int MAX_SIGNAL_RECEIVERS_PER_ROOM = 0;
public static final int MAX_SENDERS_PER_RECEIVER = 5; public static final int MAX_SENDERS_PER_RECEIVER = 0;
public boolean isSignalSenderLimitReached() { public boolean isSignalSenderLimitReached() {
Set<InteractionWiredEffect> existing = this.getSignalSenders(); return false;
return existing != null && existing.size() >= MAX_SIGNAL_SENDERS_PER_ROOM;
} }
public boolean isSignalReceiverLimitReached() { public boolean isSignalReceiverLimitReached() {
Set<InteractionWiredTrigger> existing = this.wiredTriggers.get(WiredTriggerType.RECEIVE_SIGNAL); return false;
return existing != null && existing.size() >= MAX_SIGNAL_RECEIVERS_PER_ROOM;
} }
public int countSendersTargetingReceiver(int receiverItemId, InteractionWiredEffect excludeSender) { public int countSendersTargetingReceiver(int receiverItemId, InteractionWiredEffect excludeSender) {
@@ -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<SupportedLanguage> FREE_SUPPORTED_LANGUAGES = buildFreeSupportedLanguages();
private final Map<String, CachedTranslation> translationCache = Collections.synchronizedMap(
new LinkedHashMap<String, CachedTranslation>(128, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, CachedTranslation> eldest) {
return this.size() > MAX_TRANSLATION_CACHE_SIZE;
}
});
private final Map<String, CachedLanguages> languagesCache = Collections.synchronizedMap(
new LinkedHashMap<String, CachedLanguages>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, CachedLanguages> 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<SupportedLanguage> 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<SupportedLanguage> buildFreeSupportedLanguages() {
ArrayList<SupportedLanguage> 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<SupportedLanguage> 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<SupportedLanguage> languages;
private SupportedLanguagesResponse(boolean success, String errorMessage, List<SupportedLanguage> languages) {
this.success = success;
this.errorMessage = errorMessage == null ? "" : errorMessage;
this.languages = languages == null ? Collections.emptyList() : languages;
}
public static SupportedLanguagesResponse success(List<SupportedLanguage> 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<SupportedLanguage> 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<SupportedLanguage> languages;
private CachedLanguages(List<SupportedLanguage> languages) {
this.createdAt = System.currentTimeMillis();
this.languages = languages;
}
private boolean isExpired() {
return (System.currentTimeMillis() - this.createdAt) > CACHE_TTL_MS;
}
}
}
@@ -23,6 +23,8 @@ public class HabboInventory {
private ItemsComponent itemsComponent; private ItemsComponent itemsComponent;
private PetsComponent petsComponent; private PetsComponent petsComponent;
private PrefixesComponent prefixesComponent; private PrefixesComponent prefixesComponent;
private NickIconsComponent nickIconsComponent;
private UserVisualSettingsComponent userVisualSettingsComponent;
public HabboInventory(Habbo habbo) { public HabboInventory(Habbo habbo) {
this.habbo = habbo; this.habbo = habbo;
@@ -68,6 +70,18 @@ public class HabboInventory {
LOGGER.error("Caught exception", e); 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); this.items = MarketPlace.getOwnOffers(this.habbo);
} }
@@ -127,6 +141,22 @@ public class HabboInventory {
this.prefixesComponent = prefixesComponent; 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() { public void dispose() {
this.badgesComponent.dispose(); this.badgesComponent.dispose();
this.botsComponent.dispose(); this.botsComponent.dispose();
@@ -135,6 +165,8 @@ public class HabboInventory {
this.petsComponent.dispose(); this.petsComponent.dispose();
this.wardrobeComponent.dispose(); this.wardrobeComponent.dispose();
this.prefixesComponent.dispose(); this.prefixesComponent.dispose();
this.nickIconsComponent.dispose();
this.userVisualSettingsComponent.dispose();
this.badgesComponent = null; this.badgesComponent = null;
this.botsComponent = null; this.botsComponent = null;
@@ -143,6 +175,8 @@ public class HabboInventory {
this.petsComponent = null; this.petsComponent = null;
this.wardrobeComponent = null; this.wardrobeComponent = null;
this.prefixesComponent = null; this.prefixesComponent = null;
this.nickIconsComponent = null;
this.userVisualSettingsComponent = null;
} }
public void addMarketplaceOffer(MarketPlaceOffer marketPlaceOffer) { public void addMarketplaceOffer(MarketPlaceOffer marketPlaceOffer) {
@@ -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, "", "", "", "", "");
}
}
@@ -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;
}
}
@@ -15,6 +15,12 @@ public class UserPrefix implements Runnable {
private String color; private String color;
private String icon; private String icon;
private String effect; 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 active;
private boolean needsInsert; private boolean needsInsert;
private boolean needsUpdate; private boolean needsUpdate;
@@ -29,6 +35,12 @@ public class UserPrefix implements Runnable {
if (this.icon == null) this.icon = ""; if (this.icon == null) this.icon = "";
this.effect = set.getString("effect"); this.effect = set.getString("effect");
if (this.effect == null) this.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.active = set.getBoolean("active");
this.needsInsert = false; this.needsInsert = false;
this.needsUpdate = 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) { 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.id = 0;
this.userId = userId; this.userId = userId;
this.text = text; this.text = text;
this.color = color; this.color = color;
this.icon = icon != null ? icon : ""; this.icon = icon != null ? icon : "";
this.effect = effect != null ? effect : ""; 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.active = false;
this.needsInsert = true; this.needsInsert = true;
this.needsUpdate = false; this.needsUpdate = false;
@@ -54,14 +76,20 @@ public class UserPrefix implements Runnable {
if (this.needsInsert) { if (this.needsInsert) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement( 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.RETURN_GENERATED_KEYS)) {
statement.setInt(1, this.userId); statement.setInt(1, this.userId);
statement.setString(2, this.text); statement.setString(2, this.text);
statement.setString(3, this.color); statement.setString(3, this.color);
statement.setString(4, this.icon); statement.setString(4, this.icon);
statement.setString(5, this.effect); 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(); statement.execute();
try (ResultSet set = statement.getGeneratedKeys()) { try (ResultSet set = statement.getGeneratedKeys()) {
if (set.next()) { if (set.next()) {
@@ -82,14 +110,20 @@ public class UserPrefix implements Runnable {
} else if (this.needsUpdate) { } else if (this.needsUpdate) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement( 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(1, this.text);
statement.setString(2, this.color); statement.setString(2, this.color);
statement.setString(3, this.icon); statement.setString(3, this.icon);
statement.setString(4, this.effect); statement.setString(4, this.effect);
statement.setBoolean(5, this.active); statement.setString(5, this.font);
statement.setInt(6, this.id); statement.setBoolean(6, this.active);
statement.setInt(7, this.userId); 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(); statement.execute();
} }
this.needsUpdate = false; this.needsUpdate = false;
@@ -109,6 +143,13 @@ public class UserPrefix implements Runnable {
public void setIcon(String icon) { this.icon = icon != null ? icon : ""; } public void setIcon(String icon) { this.icon = icon != null ? icon : ""; }
public String getEffect() { return this.effect; } public String getEffect() { return this.effect; }
public void setEffect(String effect) { this.effect = effect != null ? 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 boolean isActive() { return this.active; }
public void setActive(boolean 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 needsUpdate(boolean needsUpdate) { this.needsUpdate = needsUpdate; }
public void needsInsert(boolean needsInsert) { this.needsInsert = needsInsert; } public void needsInsert(boolean needsInsert) { this.needsInsert = needsInsert; }
public void needsDelete(boolean needsDelete) { this.needsDelete = needsDelete; } 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;
}
}
} }
@@ -1,7 +1,6 @@
package com.eu.habbo.habbohotel.users.inventory; package com.eu.habbo.habbohotel.users.inventory;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.database.SqlQueries;
import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInventory; 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.InventoryItemRemovedEvent;
import com.eu.habbo.plugin.events.inventory.InventoryItemsAddedEvent; import com.eu.habbo.plugin.events.inventory.InventoryItemsAddedEvent;
import gnu.trove.TCollections; import gnu.trove.TCollections;
import gnu.trove.iterator.TIntObjectIterator;
import gnu.trove.map.TIntObjectMap; import gnu.trove.map.TIntObjectMap;
import gnu.trove.map.hash.THashMap; import gnu.trove.map.hash.THashMap;
import gnu.trove.map.hash.TIntObjectHashMap; import gnu.trove.map.hash.TIntObjectHashMap;
@@ -18,9 +18,11 @@ import gnu.trove.set.hash.THashSet;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.NoSuchElementException;
import java.util.List;
public class ItemsComponent { public class ItemsComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(ItemsComponent.class); private static final Logger LOGGER = LoggerFactory.getLogger(ItemsComponent.class);
@@ -37,23 +39,25 @@ public class ItemsComponent {
public static THashMap<Integer, HabboItem> loadItems(Habbo habbo) { public static THashMap<Integer, HabboItem> loadItems(Habbo habbo) {
THashMap<Integer, HabboItem> itemsList = new THashMap<>(); THashMap<Integer, HabboItem> itemsList = new THashMap<>();
try { 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")) {
SqlQueries.forEach( statement.setInt(1, 0);
"SELECT * FROM items WHERE room_id = ? AND user_id = ?", statement.setInt(2, habbo.getHabboInfo().getId());
rs -> { try (ResultSet set = statement.executeQuery()) {
try { while (set.next()) {
HabboItem item = Emulator.getGameEnvironment().getItemManager().loadHabboItem(rs); try {
if (item != null) { HabboItem item = Emulator.getGameEnvironment().getItemManager().loadHabboItem(set);
itemsList.put(rs.getInt("id"), item);
} else { if (item != null) {
LOGGER.error("Failed to load HabboItem: {}", rs.getInt("id")); itemsList.put(set.getInt("id"), item);
} } else {
} catch (SQLException e) { LOGGER.error("Failed to load HabboItem: {}", set.getInt("id"));
LOGGER.error("Caught SQL exception", e);
} }
}, } catch (SQLException e) {
0, habbo.getHabboInfo().getId()); LOGGER.error("Caught SQL exception", e);
} catch (SqlQueries.DataAccessException e) { }
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e); LOGGER.error("Caught SQL exception", e);
} }
@@ -147,45 +151,70 @@ public class ItemsComponent {
public void dispose() { public void dispose() {
synchronized (this.items) { synchronized (this.items) {
if (!this.items.isEmpty()) { TIntObjectIterator<HabboItem> items = this.items.iterator();
List<HabboItem> updates = new ArrayList<>();
List<HabboItem> 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);
}
}
try { if (items == null) {
if (!deletes.isEmpty()) { LOGGER.error("Items is NULL!");
SqlQueries.batchUpdate( return;
"DELETE FROM items WHERE id = ?", }
deletes,
(ps, item) -> ps.setInt(1, item.getId())); 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()) { } catch (SQLException e) {
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) {
LOGGER.error("Caught SQL exception during batch item save", e); LOGGER.error("Caught SQL exception during batch item save", e);
} }
} }
@@ -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<UserNickIcon> 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<UserNickIcon> 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();
}
}
}
@@ -62,6 +62,15 @@ public class PrefixesComponent {
return null; 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) { public void addPrefix(UserPrefix prefix) {
synchronized (this.prefixes) { synchronized (this.prefixes) {
this.prefixes.add(prefix); this.prefixes.add(prefix);
@@ -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<String> 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<String> 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;
}
}
@@ -77,6 +77,22 @@ public interface IWiredEffect {
return false; 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. * Simulate this effect's execution and record intended state changes.
* <p> * <p>
@@ -112,7 +112,7 @@ public final class WiredContext {
this.state = state; this.state = state;
this.legacySettings = legacySettings; this.legacySettings = legacySettings;
this.contextVariables = (event.getContextVariableScope() != null) this.contextVariables = (event.getContextVariableScope() != null)
? event.getContextVariableScope() ? event.getContextVariableScope().copy()
: new WiredContextVariableScope(); : new WiredContextVariableScope();
this.targets = new WiredTargets(); this.targets = new WiredTargets();
@@ -26,6 +26,7 @@ import com.eu.habbo.habbohotel.wired.api.WiredStack;
import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer; 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.WiredStackExecutedEvent;
import com.eu.habbo.plugin.events.furniture.wired.WiredStackTriggeredEvent; import com.eu.habbo.plugin.events.furniture.wired.WiredStackTriggeredEvent;
import gnu.trove.map.hash.THashMap; 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 */ /** Cache room+eventType+sourceItemId -> matching stacks for source-triggered timer events */
private final ConcurrentHashMap<String, List<WiredStack>> sourceStacksByTriggerKey; private final ConcurrentHashMap<String, List<WiredStack>> sourceStacksByTriggerKey;
/** Track filter-selector animation tokens so rapid executions do not reset newer animations */
private final ConcurrentHashMap<Integer, Long> filteredSelectorAnimationTokens;
/** /**
* Create a new wired engine. * Create a new wired engine.
* *
@@ -151,6 +155,7 @@ public final class WiredEngine {
this.bannedRooms = new ConcurrentHashMap<>(); this.bannedRooms = new ConcurrentHashMap<>();
this.roomDiagnostics = new ConcurrentHashMap<>(); this.roomDiagnostics = new ConcurrentHashMap<>();
this.sourceStacksByTriggerKey = new ConcurrentHashMap<>(); this.sourceStacksByTriggerKey = new ConcurrentHashMap<>();
this.filteredSelectorAnimationTokens = new ConcurrentHashMap<>();
} }
/** /**
@@ -426,6 +431,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors); applySelectionFilterExtras(stack, ctx, executedSelectors);
} }
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions);
List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution);
boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event); boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event);
@@ -541,6 +550,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors); applySelectionFilterExtras(stack, ctx, executedSelectors);
} }
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions);
List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution);
boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event); boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event);
@@ -627,6 +640,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors); applySelectionFilterExtras(stack, ctx, executedSelectors);
} }
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions);
List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution);
return !executableEffects.isEmpty(); return !executableEffects.isEmpty();
@@ -660,6 +677,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors); applySelectionFilterExtras(stack, ctx, executedSelectors);
} }
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, false); boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, false);
if (!conditionsPassedForExecution) { if (!conditionsPassedForExecution) {
return false; return false;
@@ -1011,9 +1032,27 @@ public final class WiredEngine {
if (effects.isEmpty()) return Collections.emptyList(); if (effects.isEmpty()) return Collections.emptyList();
List<InteractionWiredEffect> executedSelectors = new ArrayList<>(); List<InteractionWiredEffect> executedSelectors = new ArrayList<>();
List<IWiredEffect> immediateSelectors = new ArrayList<>();
List<IWiredEffect> deferredSelectors = new ArrayList<>();
for (IWiredEffect effect : effects) { for (IWiredEffect effect : effects) {
if (!effect.isSelector()) continue; 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<IWiredEffect> selectors, WiredContext ctx, List<InteractionWiredEffect> executedSelectors) {
for (IWiredEffect effect : selectors) {
if (effect.requiresActor() && !ctx.hasActor()) { if (effect.requiresActor() && !ctx.hasActor()) {
continue; continue;
} }
@@ -1022,14 +1061,17 @@ public final class WiredEngine {
try { try {
effect.execute(ctx); effect.execute(ctx);
if (effect instanceof InteractionWiredEffect) { 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) { } catch (Exception e) {
LOGGER.warn("Error executing selector: {}", e.getMessage()); LOGGER.warn("Error executing selector: {}", e.getMessage());
} }
} }
return executedSelectors;
} }
private void finalizeSelectors(List<InteractionWiredEffect> executedSelectors, WiredContext ctx, long currentTime) { private void finalizeSelectors(List<InteractionWiredEffect> executedSelectors, WiredContext ctx, long currentTime) {
@@ -1042,7 +1084,56 @@ public final class WiredEngine {
for (InteractionWiredEffect wiredEffect : executedSelectors) { for (InteractionWiredEffect wiredEffect : executedSelectors) {
wiredEffect.setCooldown(currentTime); 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); WiredSelectionFilterSupport.applySelectorFilters(room, stack.triggerItem(), ctx);
} }
private boolean selectorsHaveRequiredTargets(List<InteractionWiredEffect> 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. * Schedule a delayed effect execution.
*/ */
@@ -151,8 +151,20 @@ public final class WiredSourceUtil {
selectorCtx.setIncludeWiredSelectorItems(originalCtx.includeWiredSelectorItems()); selectorCtx.setIncludeWiredSelectorItems(originalCtx.includeWiredSelectorItems());
List<InteractionWiredEffect> selectorEffects = getOrderedSelectorEffects(originalCtx, room, triggerItem); List<InteractionWiredEffect> 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<InteractionWiredEffect> selectorEffects, boolean deferred) {
for (InteractionWiredEffect effect : selectorEffects) { for (InteractionWiredEffect effect : selectorEffects) {
if (effect == null || effect.usesExistingSelectorTargets() != deferred) {
continue;
}
if (effect.requiresActor() && !selectorCtx.hasActor()) { if (effect.requiresActor() && !selectorCtx.hasActor()) {
continue; continue;
} }
@@ -163,10 +175,6 @@ public final class WiredSourceUtil {
} catch (Exception ignored) { } catch (Exception ignored) {
} }
} }
applySelectionFilterExtras(room, triggerItem, selectorCtx);
return selectorCtx;
} }
private static WiredContext cloneSelectorContext(WiredContext originalCtx, boolean includeWiredItems) { private static WiredContext cloneSelectorContext(WiredContext originalCtx, boolean includeWiredItems) {
@@ -64,8 +64,17 @@ public final class WiredTextInputCaptureSupport {
return trigger.matches(stack.triggerItem(), event) ? CaptureResult.matched(new LinkedHashMap<>()) : CaptureResult.noMatch(); 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 (!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(); return CaptureResult.noMatch();
} }
@@ -78,12 +87,28 @@ public final class WiredTextInputCaptureSupport {
Integer resolvedValue = capturer.resolveCapturedValue(room, capture.getValue()); Integer resolvedValue = capturer.resolveCapturedValue(room, capture.getValue());
if (resolvedValue == null) { 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(); return CaptureResult.noMatch();
} }
capturedValues.put(capturer.getVariableItemId(), resolvedValue); 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); return CaptureResult.matched(capturedValues);
} }
@@ -108,12 +133,13 @@ public final class WiredTextInputCaptureSupport {
return capturers; return capturers;
} }
private static MatchResult matchTemplate(WiredTriggerHabboSaysKeyword trigger, String rawText, Map<String, WiredExtraTextInputVariable> capturersByName) { private static MatchResult matchTemplate(WiredTriggerHabboSaysKeyword trigger, String rawText, Map<String, WiredExtraTextInputVariable> capturersByName, Room room) {
String text = rawText != null ? rawText.trim() : ""; String text = rawText != null ? rawText : "";
String normalizedText = text.trim();
String template = trigger.getKey() != null ? trigger.getKey().trim() : ""; String template = trigger.getKey() != null ? trigger.getKey().trim() : "";
if (trigger.getMatchMode() == MATCH_ALL_WORDS && template.isEmpty()) { if (trigger.getMatchMode() == MATCH_ALL_WORDS && template.isEmpty()) {
if (capturersByName.size() != 1 || text.isEmpty()) { if (capturersByName.size() != 1 || normalizedText.isEmpty()) {
return MatchResult.noMatch(); return MatchResult.noMatch();
} }
@@ -123,12 +149,24 @@ public final class WiredTextInputCaptureSupport {
return MatchResult.matched(captures); 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); TemplatePattern pattern = buildPattern(template);
if (pattern == null) { if (pattern == null) {
return MatchResult.noMatch(); 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(); boolean matches = (trigger.getMatchMode() == MATCH_CONTAINS) ? matcher.find() : matcher.matches();
if (!matches) { if (!matches) {
return MatchResult.noMatch(); return MatchResult.noMatch();
@@ -142,12 +180,136 @@ public final class WiredTextInputCaptureSupport {
} }
String capturedValue = matcher.group(index + 1); String capturedValue = matcher.group(index + 1);
captures.put(placeholderName, capturedValue != null ? capturedValue.trim() : ""); captures.put(placeholderName, normalizeCapturedValue(capturedValue));
} }
return MatchResult.matched(captures); return MatchResult.matched(captures);
} }
private static MatchResult matchAdjacentCapturers(String template, String rawText, Map<String, WiredExtraTextInputVariable> 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<String> 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<String, String> 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) { private static TemplatePattern buildPattern(String template) {
if (template == null || template.isEmpty()) { if (template == null || template.isEmpty()) {
return null; return null;
@@ -160,7 +322,7 @@ public final class WiredTextInputCaptureSupport {
while (matcher.find()) { while (matcher.find()) {
regex.append(Pattern.quote(template.substring(cursor, matcher.start()))); 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() : ""; String placeholderName = matcher.group(1) != null ? matcher.group(1).trim().toLowerCase() : "";
placeholderNames.add(placeholderName); 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); 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) { public static void applyToContext(WiredContext ctx, Room room, CaptureResult captureResult) {
if (ctx == null || room == null || captureResult == null || !captureResult.matches || captureResult.capturedValues.isEmpty()) { if (ctx == null || room == null || captureResult == null || !captureResult.matches || captureResult.capturedValues.isEmpty()) {
return; return;
@@ -32,6 +32,8 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
public final class WiredTextPlaceholderUtil { public final class WiredTextPlaceholderUtil {
private static final char PRESERVED_SPACE = '\u00A0';
private WiredTextPlaceholderUtil() { 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) { public static boolean requiresActor(Room room, HabboItem stackItem) {
@@ -275,7 +311,7 @@ public final class WiredTextPlaceholderUtil {
} }
String value = resolveRoomVariableValue(room, extra); 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<String> collectContextVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) { private static List<String> collectContextVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) {
@@ -284,7 +320,7 @@ public final class WiredTextPlaceholderUtil {
} }
String value = resolveContextVariableValue(ctx, extra); 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) { private static String resolveUserVariableValue(Room room, RoomUnit roomUnit, WiredExtraTextOutputVariable extra) {
@@ -11,6 +11,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
public final class WiredVariableTextConnectorSupport { public final class WiredVariableTextConnectorSupport {
private static final String PRESERVED_SPACE = "\u00A0";
private WiredVariableTextConnectorSupport() { private WiredVariableTextConnectorSupport() {
} }
@@ -71,7 +73,7 @@ public final class WiredVariableTextConnectorSupport {
Map<Integer, String> mappings = connector.getMappings(); Map<Integer, String> mappings = connector.getMappings();
if (mappings.containsKey(value)) { if (mappings.containsKey(value)) {
String mappedValue = mappings.get(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; return null;
} }
String normalizedText = text.trim(); String normalizedText = normalizePreservedSpaces(text);
if (normalizedText.isEmpty()) {
return null;
}
for (WiredExtraVariableTextConnector connector : getConnectors(room, definitionItemId)) { for (WiredExtraVariableTextConnector connector : getConnectors(room, definitionItemId)) {
Integer mappedValue = connector.resolveValue(normalizedText); Integer mappedValue = connector.resolveValue(normalizedText);
@@ -97,4 +96,12 @@ public final class WiredVariableTextConnectorSupport {
return null; return null;
} }
private static String preserveSpaces(String value) {
return value.replace(" ", PRESERVED_SPACE);
}
private static String normalizePreservedSpaces(String value) {
return value.replace(PRESERVED_SPACE, " ");
}
} }
@@ -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.helper.RequestTalentTrackEvent;
import com.eu.habbo.messages.incoming.hotelview.*; import com.eu.habbo.messages.incoming.hotelview.*;
import com.eu.habbo.messages.incoming.inventory.*; 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.inventory.prefixes.*;
import com.eu.habbo.messages.incoming.modtool.*; import com.eu.habbo.messages.incoming.modtool.*;
import com.eu.habbo.messages.incoming.navigator.*; 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.promotions.UpdateRoomPromotionEvent;
import com.eu.habbo.messages.incoming.rooms.users.*; import com.eu.habbo.messages.incoming.rooms.users.*;
import com.eu.habbo.messages.incoming.trading.*; 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.RequestResolutionEvent;
import com.eu.habbo.messages.incoming.unknown.UnknownEvent1; import com.eu.habbo.messages.incoming.unknown.UnknownEvent1;
import com.eu.habbo.messages.incoming.users.*; import com.eu.habbo.messages.incoming.users.*;
@@ -117,6 +120,7 @@ public class PacketManager {
this.registerGuilds(); this.registerGuilds();
this.registerPets(); this.registerPets();
this.registerWired(); this.registerWired();
this.registerTranslation();
this.registerAchievements(); this.registerAchievements();
this.registerFloorPlanEditor(); this.registerFloorPlanEditor();
this.registerAmbassadors(); this.registerAmbassadors();
@@ -409,6 +413,13 @@ public class PacketManager {
this.registerHandler(Incoming.SetActivePrefixEvent, SetActivePrefixEvent.class); this.registerHandler(Incoming.SetActivePrefixEvent, SetActivePrefixEvent.class);
this.registerHandler(Incoming.DeletePrefixEvent, DeletePrefixEvent.class); this.registerHandler(Incoming.DeletePrefixEvent, DeletePrefixEvent.class);
this.registerHandler(Incoming.PurchasePrefixEvent, PurchasePrefixEvent.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 { void registerRooms() throws Exception {
@@ -635,6 +646,11 @@ public class PacketManager {
this.registerHandler(Incoming.WiredUserInspectMoveEvent, WiredUserInspectMoveEvent.class); 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 { void registerUnknown() throws Exception {
this.registerHandler(Incoming.RequestResolutionEvent, RequestResolutionEvent.class); this.registerHandler(Incoming.RequestResolutionEvent, RequestResolutionEvent.class);
this.registerHandler(Incoming.RequestTalenTrackEvent, RequestTalentTrackEvent.class); this.registerHandler(Incoming.RequestTalenTrackEvent, RequestTalentTrackEvent.class);
@@ -419,6 +419,8 @@ public class Incoming {
public static final int WiredUserVariableUpdateEvent = 10025; public static final int WiredUserVariableUpdateEvent = 10025;
public static final int WiredUserVariableManageEvent = 10026; public static final int WiredUserVariableManageEvent = 10026;
public static final int WiredUserInspectMoveEvent = 10027; 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 RequestInventoryPetDelete = 10030;
public static final int RequestInventoryBadgeDelete = 10031; public static final int RequestInventoryBadgeDelete = 10031;
@@ -448,6 +450,11 @@ public class Incoming {
public static final int SetActivePrefixEvent = 7012; public static final int SetActivePrefixEvent = 7012;
public static final int DeletePrefixEvent = 7013; public static final int DeletePrefixEvent = 7013;
public static final int PurchasePrefixEvent = 7014; 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 // YouTube Room Broadcast
public static final int YouTubeRoomPlayEvent = 8001; public static final int YouTubeRoomPlayEvent = 8001;
@@ -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 : "";
}
}
@@ -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()));
}
}
@@ -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());
}
}
}
@@ -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);
}
}
}
@@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.users.UserPrefix;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; 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.inventory.prefixes.PrefixReceivedComposer;
import com.eu.habbo.messages.outgoing.users.UserCreditsComposer; import com.eu.habbo.messages.outgoing.users.UserCreditsComposer;
import com.eu.habbo.messages.outgoing.users.UserCurrencyComposer; import com.eu.habbo.messages.outgoing.users.UserCurrencyComposer;
@@ -19,6 +20,7 @@ import java.sql.SQLException;
public class PurchasePrefixEvent extends MessageHandler { public class PurchasePrefixEvent extends MessageHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(PurchasePrefixEvent.class); private static final Logger LOGGER = LoggerFactory.getLogger(PurchasePrefixEvent.class);
private static final String[] ALLOWED_FONTS = { "", "pixel", "cherry", "vampiro" };
@Override @Override
public int getRatelimit() { public int getRatelimit() {
@@ -31,6 +33,7 @@ public class PurchasePrefixEvent extends MessageHandler {
String color = this.packet.readString(); String color = this.packet.readString();
String icon = this.packet.readString(); String icon = this.packet.readString();
String effect = this.packet.readString(); String effect = this.packet.readString();
String font = this.packet.readString();
Habbo habbo = this.client.getHabbo(); Habbo habbo = this.client.getHabbo();
@@ -42,6 +45,9 @@ public class PurchasePrefixEvent extends MessageHandler {
int priceCredits = getSettingInt("price_credits", 5); int priceCredits = getSettingInt("price_credits", 5);
int pricePoints = getSettingInt("price_points", 0); int pricePoints = getSettingInt("price_points", 0);
int pointsType = getSettingInt("points_type", 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 // Validate text
text = text.trim(); text = text.trim();
@@ -72,43 +78,67 @@ public class PurchasePrefixEvent extends MessageHandler {
return; 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 // 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.")); this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough credits."));
return; return;
} }
int totalPricePointsSameType = pricePoints + ((fontPricePoints > 0 && fontPointsType == pointsType && !font.isEmpty()) ? fontPricePoints : 0);
// Check points // 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.")); this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points."));
return; return;
} }
// Deduct currency // Deduct currency
if (priceCredits > 0) { if (totalPriceCredits > 0) {
habbo.getHabboInfo().addCredits(-priceCredits); habbo.getHabboInfo().addCredits(-totalPriceCredits);
this.client.sendResponse(new UserCreditsComposer(habbo)); this.client.sendResponse(new UserCreditsComposer(habbo));
} }
if (pricePoints > 0) { if (totalPricePointsSameType > 0) {
habbo.getHabboInfo().addCurrencyAmount(pointsType, -pricePoints); habbo.getHabboInfo().addCurrencyAmount(pointsType, -totalPricePointsSameType);
this.client.sendResponse(new UserCurrencyComposer(habbo)); this.client.sendResponse(new UserCurrencyComposer(habbo));
} }
// Validate icon (allow empty or known icon names) if (!font.isEmpty() && fontPricePoints > 0 && fontPointsType != pointsType) {
if (icon == null) icon = ""; habbo.getHabboInfo().addCurrencyAmount(fontPointsType, -fontPricePoints);
icon = icon.trim(); this.client.sendResponse(new UserCurrencyComposer(habbo));
}
// Validate effect
if (effect == null) effect = "";
effect = effect.trim();
// Create prefix // 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 prefix.run(); // Insert into DB synchronously to get the ID
habbo.getInventory().getPrefixesComponent().addPrefix(prefix); habbo.getInventory().getPrefixesComponent().addPrefix(prefix);
this.client.sendResponse(new PrefixReceivedComposer(prefix)); this.client.sendResponse(new PrefixReceivedComposer(prefix));
this.client.sendResponse(new UserNickIconsComposer(habbo));
} }
private int getSettingInt(String key, int defaultValue) { private int getSettingInt(String key, int defaultValue) {
@@ -142,4 +172,14 @@ public class PurchasePrefixEvent extends MessageHandler {
} }
return false; return false;
} }
private boolean isAllowedFont(String font) {
for (String allowedFont : ALLOWED_FONTS) {
if (allowedFont.equals(font)) {
return true;
}
}
return false;
}
} }
@@ -3,6 +3,8 @@ package com.eu.habbo.messages.incoming.inventory.prefixes;
import com.eu.habbo.habbohotel.users.UserPrefix; import com.eu.habbo.habbohotel.users.UserPrefix;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.inventory.prefixes.ActivePrefixUpdatedComposer; 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 { public class SetActivePrefixEvent extends MessageHandler {
@Override @Override
@@ -12,6 +14,11 @@ public class SetActivePrefixEvent extends MessageHandler {
if (prefixId == 0) { if (prefixId == 0) {
this.client.getHabbo().getInventory().getPrefixesComponent().deactivateAll(); this.client.getHabbo().getInventory().getPrefixesComponent().deactivateAll();
this.client.sendResponse(new ActivePrefixUpdatedComposer(null)); 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; return;
} }
@@ -21,5 +28,10 @@ public class SetActivePrefixEvent extends MessageHandler {
this.client.getHabbo().getInventory().getPrefixesComponent().setActive(prefixId); this.client.getHabbo().getInventory().getPrefixesComponent().setActive(prefixId);
this.client.sendResponse(new ActivePrefixUpdatedComposer(prefix)); 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());
}
} }
} }
@@ -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());
}
}
}
@@ -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;
}
}
@@ -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());
});
}
}
@@ -9,6 +9,7 @@ import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.generic.alerts.UpdateFailedComposer; 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; import com.eu.habbo.messages.outgoing.wired.WiredSavedComposer;
public class WiredEffectSaveDataEvent extends MessageHandler { public class WiredEffectSaveDataEvent extends MessageHandler {
@@ -39,6 +40,16 @@ public class WiredEffectSaveDataEvent extends MessageHandler {
if (saved) { if (saved) {
this.client.sendResponse(new WiredSavedComposer()); this.client.sendResponse(new WiredSavedComposer());
if (effect != null) { 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); effect.needsUpdate(true);
Emulator.getThreading().run(effect); Emulator.getThreading().run(effect);
} else { } else {
@@ -124,6 +124,8 @@ public class Outgoing {
public final static int WiredRoomSettingsDataComposer = 5102; // CUSTOM public final static int WiredRoomSettingsDataComposer = 5102; // CUSTOM
public final static int WiredUserVariablesDataComposer = 5103; // CUSTOM public final static int WiredUserVariablesDataComposer = 5103; // CUSTOM
public final static int ConfInvisStateComposer = 5104; // 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 AreaHideComposer = 6001; // CUSTOM
public final static int RoomPaintComposer = 2454; // PRODUCTION-201611291003-338511768 public final static int RoomPaintComposer = 2454; // PRODUCTION-201611291003-338511768
public final static int MarketplaceConfigComposer = 1823; // 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 UserPrefixesComposer = 7001;
public static final int PrefixReceivedComposer = 7002; public static final int PrefixReceivedComposer = 7002;
public static final int ActivePrefixUpdatedComposer = 7003; public static final int ActivePrefixUpdatedComposer = 7003;
public static final int UserNickIconsComposer = 7004;
public static final int AvailableCommandsComposer = 4050; public static final int AvailableCommandsComposer = 4050;
// YouTube Room Broadcast // YouTube Room Broadcast
@@ -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<String, UserNickIcon> ownedByKey = new HashMap<>();
List<UserNickIcon> ownedNickIcons = this.habbo.getInventory().getNickIconsComponent().getNickIcons();
for (UserNickIcon nickIcon : ownedNickIcons) {
ownedByKey.put(nickIcon.getIconKey().toLowerCase(), nickIcon);
}
Map<Integer, UserPrefix> ownedPrefixesByCatalogId = new HashMap<>();
List<UserPrefix> 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<CatalogNickIcon> 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<CatalogPrefix> 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;
}
}
}
@@ -22,12 +22,14 @@ public class ActivePrefixUpdatedComposer extends MessageComposer {
this.response.appendString(this.prefix.getColor()); this.response.appendString(this.prefix.getColor());
this.response.appendString(this.prefix.getIcon()); this.response.appendString(this.prefix.getIcon());
this.response.appendString(this.prefix.getEffect()); this.response.appendString(this.prefix.getEffect());
this.response.appendString(this.prefix.getFont());
} else { } else {
this.response.appendInt(0); this.response.appendInt(0);
this.response.appendString(""); this.response.appendString("");
this.response.appendString(""); this.response.appendString("");
this.response.appendString(""); this.response.appendString("");
this.response.appendString(""); this.response.appendString("");
this.response.appendString("");
} }
return this.response; return this.response;
@@ -20,6 +20,7 @@ public class PrefixReceivedComposer extends MessageComposer {
this.response.appendString(this.prefix.getColor()); this.response.appendString(this.prefix.getColor());
this.response.appendString(this.prefix.getIcon()); this.response.appendString(this.prefix.getIcon());
this.response.appendString(this.prefix.getEffect()); this.response.appendString(this.prefix.getEffect());
this.response.appendString(this.prefix.getFont());
return this.response; return this.response;
} }
} }
@@ -30,6 +30,7 @@ public class UserPrefixesComposer extends MessageComposer {
this.response.appendString(prefix.getColor()); this.response.appendString(prefix.getColor());
this.response.appendString(prefix.getIcon()); this.response.appendString(prefix.getIcon());
this.response.appendString(prefix.getEffect()); this.response.appendString(prefix.getEffect());
this.response.appendString(prefix.getFont());
this.response.appendInt(prefix.isActive() ? 1 : 0); this.response.appendInt(prefix.isActive() ? 1 : 0);
} }
@@ -1,6 +1,7 @@
package com.eu.habbo.messages.outgoing.rooms.users; package com.eu.habbo.messages.outgoing.rooms.users;
import com.eu.habbo.habbohotel.users.Habbo; 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.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing; 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().getInfostandStand());
this.response.appendInt(this.habbo.getHabboInfo().getInfostandOverlay()); this.response.appendInt(this.habbo.getHabboInfo().getInfostandOverlay());
this.response.appendInt(this.habbo.getHabboInfo().getInfostandCardBg()); 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; return this.response;
} }
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.bots.Bot; import com.eu.habbo.habbohotel.bots.Bot;
import com.eu.habbo.habbohotel.guilds.Guild; import com.eu.habbo.habbohotel.guilds.Guild;
import com.eu.habbo.habbohotel.users.Habbo; 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.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing; import com.eu.habbo.messages.outgoing.Outgoing;
@@ -67,6 +68,14 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendString(""); this.response.appendString("");
this.response.appendInt(this.habbo.getHabboStats().getAchievementScore()); this.response.appendInt(this.habbo.getHabboStats().getAchievementScore());
this.response.appendBoolean(true); 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.appendString(this.habbo.getHabboInfo().getRoomEntryMethod());
this.response.appendInt(this.habbo.getHabboInfo().getRoomEntryTeleportId()); this.response.appendInt(this.habbo.getHabboInfo().getRoomEntryTeleportId());
} else if (this.habbos != null) { } else if (this.habbos != null) {
@@ -101,6 +110,14 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendString(""); this.response.appendString("");
this.response.appendInt(habbo.getHabboStats().getAchievementScore()); this.response.appendInt(habbo.getHabboStats().getAchievementScore());
this.response.appendBoolean(true); 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.appendString(habbo.getHabboInfo().getRoomEntryMethod());
this.response.appendInt(habbo.getHabboInfo().getRoomEntryTeleportId()); this.response.appendInt(habbo.getHabboInfo().getRoomEntryTeleportId());
} }
@@ -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;
}
}
@@ -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;
}
}
@@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.guilds.Guild;
import com.eu.habbo.habbohotel.messenger.Messenger; import com.eu.habbo.habbohotel.messenger.Messenger;
import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo; 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.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing; 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.getInfostandStand());
this.response.appendInt(this.habboInfo.getInfostandOverlay()); this.response.appendInt(this.habboInfo.getInfostandOverlay());
this.response.appendInt(this.habboInfo.getInfostandCardBg()); 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; return this.response;
} }
@@ -92,4 +92,4 @@ public abstract class Server {
public int getPort() { public int getPort() {
return this.port; return this.port;
} }
} }
@@ -3,6 +3,8 @@ package com.eu.habbo.networking.gameserver;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.messages.PacketManager; import com.eu.habbo.messages.PacketManager;
import com.eu.habbo.networking.gameserver.auth.AuthHttpHandler; 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.badges.BadgeHttpHandler;
import com.eu.habbo.networking.gameserver.codec.WebSocketCodec; import com.eu.habbo.networking.gameserver.codec.WebSocketCodec;
import com.eu.habbo.networking.gameserver.crypto.WsHandshakeHandler; import com.eu.habbo.networking.gameserver.crypto.WsHandshakeHandler;
@@ -53,6 +55,8 @@ public class WebSocketChannelInitializer extends ChannelInitializer<SocketChanne
ch.pipeline().addLast("httpCodec", new HttpServerCodec()); ch.pipeline().addLast("httpCodec", new HttpServerCodec());
ch.pipeline().addLast("httpAggregator", new HttpObjectAggregator(MAX_FRAME_SIZE)); ch.pipeline().addLast("httpAggregator", new HttpObjectAggregator(MAX_FRAME_SIZE));
ch.pipeline().addLast("wsHttpHandler", new WebSocketHttpHandler()); ch.pipeline().addLast("wsHttpHandler", new WebSocketHttpHandler());
ch.pipeline().addLast("nitroSecureAssetHandler", new NitroSecureAssetHandler());
ch.pipeline().addLast("nitroSecureApiHandler", new NitroSecureApiHandler());
ch.pipeline().addLast("authHttpHandler", new AuthHttpHandler()); ch.pipeline().addLast("authHttpHandler", new AuthHttpHandler());
ch.pipeline().addLast("badgeHttpHandler", new BadgeHttpHandler()); ch.pipeline().addLast("badgeHttpHandler", new BadgeHttpHandler());
ch.pipeline().addLast("wsProtocolHandler", new WebSocketServerProtocolHandler(this.wsConfig)); ch.pipeline().addLast("wsProtocolHandler", new WebSocketServerProtocolHandler(this.wsConfig));
@@ -348,6 +348,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
ok.addProperty("username", rot.username); ok.addProperty("username", rot.username);
ok.addProperty("rememberToken", rot.jwt); ok.addProperty("rememberToken", rot.jwt);
ok.addProperty("expiresAt", rot.expiresAt); ok.addProperty("expiresAt", rot.expiresAt);
ok.addProperty("rememberExpiresAt", rot.expiresAt);
AccessTokenService.Issued access = AccessTokenService.issue(rot.userId); AccessTokenService.Issued access = AccessTokenService.issue(rot.userId);
ok.addProperty("accessToken", access.token); ok.addProperty("accessToken", access.token);
ok.addProperty("accessTokenExpiresAt", access.expiresAt); ok.addProperty("accessTokenExpiresAt", access.expiresAt);
@@ -410,6 +411,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
JsonObject ok = new JsonObject(); JsonObject ok = new JsonObject();
ok.addProperty("rememberToken", rot.jwt); ok.addProperty("rememberToken", rot.jwt);
ok.addProperty("expiresAt", rot.expiresAt); ok.addProperty("expiresAt", rot.expiresAt);
ok.addProperty("rememberExpiresAt", rot.expiresAt);
AccessTokenService.Issued access = AccessTokenService.issue(rot.userId); AccessTokenService.Issued access = AccessTokenService.issue(rot.userId);
ok.addProperty("accessToken", access.token); ok.addProperty("accessToken", access.token);
ok.addProperty("accessTokenExpiresAt", access.expiresAt); ok.addProperty("accessTokenExpiresAt", access.expiresAt);
@@ -423,7 +425,7 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
private void handleLogin(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) { private void handleLogin(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
String username = readString(body, "username").trim(); String username = readString(body, "username").trim();
String password = readString(body, "password"); String password = readString(body, "password");
boolean rememberMe = readBoolean(body, "remember", false); boolean rememberMe = readBoolean(body, "remember", false) || readBoolean(body, "rememberMe", false);
if (username.isEmpty() || password.isEmpty()) { if (username.isEmpty() || password.isEmpty()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing credentials.")); sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing credentials."));
@@ -1137,7 +1139,8 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
response.headers().set("Access-Control-Allow-Credentials", "true"); 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-Methods", "GET, HEAD, POST, OPTIONS");
response.headers().set("Access-Control-Allow-Headers", "Content-Type, X-Requested-With"); 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) { private static boolean isKeepAlive(FullHttpRequest req) {
@@ -0,0 +1,290 @@
package com.eu.habbo.networking.gameserver.auth;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.*;
import io.netty.util.AttributeKey;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
public class NitroSecureApiHandler extends ChannelDuplexHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(NitroSecureApiHandler.class);
private static final String ENABLED_CONFIG = "nitro.secure.api.enabled";
private static final String API_PREFIX = "/api/";
private static final AttributeKey<Deque<SecureApiContext>> SECURE_CONTEXTS =
AttributeKey.valueOf("nitroSecureApiContexts");
private static final ConcurrentHashMap<String, Long> 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<SecureApiContext> 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<SecureApiContext> 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<SecureApiContext> 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<SecureApiContext> 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);
}
}
}
@@ -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<String, CacheEntry> 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) {}
}
@@ -44,7 +44,9 @@ public final class RememberJwtService {
} }
private static int familyTtlDays() { 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() { private static long familyTtlSeconds() {
@@ -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.ItemManager;
import com.eu.habbo.habbohotel.items.interactions.InteractionPostIt; import com.eu.habbo.habbohotel.items.interactions.InteractionPostIt;
import com.eu.habbo.habbohotel.items.interactions.InteractionRoller; 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.items.interactions.games.football.InteractionFootballGate;
import com.eu.habbo.habbohotel.messenger.Messenger; import com.eu.habbo.habbohotel.messenger.Messenger;
import com.eu.habbo.habbohotel.modtool.WordFilter; import com.eu.habbo.habbohotel.modtool.WordFilter;
@@ -116,6 +117,7 @@ public class PluginManager {
RoomManager.HOME_ROOM_ID = Emulator.getConfig().getInt("hotel.home.room"); RoomManager.HOME_ROOM_ID = Emulator.getConfig().getInt("hotel.home.room");
WiredManager.MAXIMUM_FURNI_SELECTION = Emulator.getConfig().getInt("hotel.wired.furni.selection.count"); WiredManager.MAXIMUM_FURNI_SELECTION = Emulator.getConfig().getInt("hotel.wired.furni.selection.count");
WiredManager.TELEPORT_DELAY = Emulator.getConfig().getInt("wired.effect.teleport.delay", 500); 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_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.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); WiredEngine.RATE_LIMIT_WINDOW_MS = Emulator.getConfig().getInt("wired.abuse.rate.limit.window.ms", 10000);
+26 -1
View File
@@ -10,6 +10,11 @@ db.pool.maxsize=100
# Encrypt your traffic # Encrypt your traffic
crypto.ws.enabled=0 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. #Game Configuration.
#Host IP. Most likely just 0.0.0.0 Use 127.0.0.1 if you want to play on LAN. #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.enabled=false
enc.e=3 enc.e=3
enc.n=86851dd364d5c5cece3c883171cc6ddc5760779b992482bd1e20dd296888df91b33b936a7b93f06d29e8870f703a216257dec7c81de0058fea4cc5116f75e6efc4e9113513e45357dc3fd43d4efab5963ef178b78bd61e81a14c603b24c8bcce0a12230b320045498edc29282ff0603bc7b7dae8fc1b05b52b2f301a9dc783b7 enc.n=86851dd364d5c5cece3c883171cc6ddc5760779b992482bd1e20dd296888df91b33b936a7b93f06d29e8870f703a216257dec7c81de0058fea4cc5116f75e6efc4e9113513e45357dc3fd43d4efab5963ef178b78bd61e81a14c603b24c8bcce0a12230b320045498edc29282ff0603bc7b7dae8fc1b05b52b2f301a9dc783b7
enc.d=59ae13e243392e89ded305764bdd9e92e4eafa67bb6dac7e1415e8c645b0950bccd26246fd0d4af37145af5fa026c0ec3a94853013eaae5ff1888360f4f9449ee023762ec195dff3f30ca0b08b8c947e3859877b5d7dced5c8715c58b53740b84e11fbc71349a27c31745fcefeeea57cff291099205e230e0c7c27e8e1c0512b 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