Compare commits

..

29 Commits

Author SHA1 Message Date
github-actions[bot] 8709a72b6e 🆙 Bump version to 4.1.15 [skip ci] 2026-05-12 08:55:48 +00:00
DuckieTM c331da9fbe Merge pull request #101 from duckietm/dev
Dev
2026-05-12 10:54:51 +02:00
duckietm f9a079da02 🆙 comibe SQLs 2026-05-12 09:18:22 +02:00
duckietm 89eb989c26 🆙 Refactor AuthHttpHandler for the API and Websocket 2026-05-12 09:11:43 +02:00
duckietm 47be392d8e 🆕 Added Reset password / Email and chenge username in user settings 2026-05-11 18:06:34 +02:00
duckietm d9465a0a65 🆙 Update Some security updates for guilds 2026-05-08 15:38:14 +02:00
duckietm 90314d00fe 🆙 Fix Guilds removal 2026-05-08 15:19:00 +02:00
duckietm 56c73b9d98 🆙 Small fix for the websocket, some CF users have problems with the max frame size 2026-05-08 08:03:51 +02:00
github-actions[bot] e6093f959f 🆙 Bump version to 4.1.14 [skip ci] 2026-05-06 10:51:57 +00:00
DuckieTM c854770561 Merge pull request #100 from duckietm/dev
Dev
2026-05-06 12:51:02 +02:00
DuckieTM a0b59134ee Merge pull request #99 from Lorenzune/merge-duckie-main-2026-05-06
Merge live secure runtime updates into dev
2026-05-06 07:08:37 +02:00
Lorenzune 67924289ac Complete secure config example 2026-05-06 06:27:49 +02:00
Lorenzune 26326bcc0e Merge remote-tracking branch 'duckie/main' into merge-duckie-main-2026-05-06
# Conflicts:
#	Database Updates/016_custom_prefixes_setup.sql
#	Database Updates/custom_nick_icons_setup.sql
#	Database Updates/remember_login_tokens.sql
#	Database Updates/wired_message_length_512.sql
#	Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java
#	Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java
#	Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java
#	Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java
#	Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpHandler.java
2026-05-06 04:23:14 +02:00
duckietm ee0613a480 🆙 Update 2026-05-05 12:50:28 +02:00
DuckieTM 37d7885663 🆙 update 2026-05-05 12:09:05 +02:00
github-actions[bot] fdf0e5d806 🆙 Bump version to 4.1.13 [skip ci] 2026-05-04 13:38:38 +00:00
DuckieTM c64d3b7b8d Merge pull request #98 from duckietm/dev
Dev
2026-05-04 15:37:38 +02:00
duckietm c2b85c0c8c 🆙 Redone Background profiles 2026-05-04 15:15:41 +02:00
duckietm f8a651b059 🆙 Security update Info stand background 2026-05-04 13:18:06 +02:00
Lorenzune 59ce829fe0 Merge duckie main into live merge branch 2026-04-25 13:52:04 +02:00
Lorenzune 9bad1eb3f6 Update secure configuration example paths 2026-04-25 13:30:00 +02:00
Lorenzune f51617d092 Add secure mode config toggles 2026-04-24 15:55:39 +02:00
Lorenzune 585af846c4 Add secure assets and remember login support 2026-04-23 16:27:01 +02:00
Lorenzune dde2c4143c checkpoint: secure config gdm and api baseline 2026-04-23 07:01:09 +02:00
Lorenzune 26999c254b Merge remote-tracking branch 'duckie/main' into duckie-live-merge-2026-04-21 2026-04-22 09:43:43 +02:00
Lorenzune dd96523496 Merge latest duckie main with UI login 2026-04-21 11:44:19 +02:00
Lorenzune 02f3ded44e Merge remote-tracking branch 'duckie-temp/main' into duckie-merge-2026-04-21
# Conflicts:
#	Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java
#	Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java
#	Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/ItemsComponent.java
#	Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java
2026-04-21 11:20:06 +02:00
Lorenzune 8bbe8640b0 WIP preserve local changes before duckie merge 2026-04-21 11:13:32 +02:00
Lorenzune 078fb3db60 Fix wired text capture and showmessage behavior 2026-04-21 08:54:02 +02:00
103 changed files with 6170 additions and 1352 deletions
+2 -3
View File
@@ -91,7 +91,7 @@ ALTER TABLE `catalog_pages`
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
ALTER TABLE `catalog_pages`
ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL','BUILDER','BOTH') NOT NULL DEFAULT 'NORMAL'
AFTER `club_only`;
@@ -878,7 +878,7 @@ ON DUPLICATE KEY UPDATE
`permission` = VALUES(`permission`),
`overridable` = VALUES(`overridable`),
`triggers_talking_furniture` = VALUES(`triggers_talking_furniture`);
ALTER TABLE `catalog_club_offers`
MODIFY COLUMN `type` ENUM('HC', 'VIP', 'BUILDERS_CLUB', 'BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC';
@@ -987,4 +987,3 @@ ALTER TABLE `catalog_pages_bc`
'builders_club_addons',
'builders_club_loyalty'
) NOT NULL DEFAULT 'default_3x3';
+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` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
@@ -5,28 +15,57 @@ CREATE TABLE IF NOT EXISTS `user_prefixes` (
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
`icon` VARCHAR(50) NOT NULL DEFAULT '',
`effect` VARCHAR(50) NOT NULL DEFAULT '',
`font` VARCHAR(50) NOT NULL DEFAULT '',
`catalog_prefix_id` INT(11) NOT NULL DEFAULT 0,
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
`points` INT(11) NOT NULL DEFAULT 0,
`points_type` INT(11) NOT NULL DEFAULT 0,
`is_custom` TINYINT(1) NOT NULL DEFAULT 1,
`active` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_user_active` (`user_id`, `active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. Prefix settings table
-- ------------------------------------------------------------
-- 2. Catalog table
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `custom_prefixes_catalog` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`display_name` VARCHAR(100) NOT NULL DEFAULT '',
`text` VARCHAR(50) NOT NULL,
`color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF',
`icon` VARCHAR(50) NOT NULL DEFAULT '',
`effect` VARCHAR(50) NOT NULL DEFAULT '',
`font` VARCHAR(50) NOT NULL DEFAULT '',
`points` INT(11) NOT NULL DEFAULT 0,
`points_type` INT(11) NOT NULL DEFAULT 0,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ------------------------------------------------------------
-- 3. User visual settings
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `user_visual_settings` (
`user_id` INT(11) NOT NULL,
`display_order` VARCHAR(50) NOT NULL DEFAULT 'icon-prefix-name',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ------------------------------------------------------------
-- 4. Prefix settings table
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
`key_name` VARCHAR(100) NOT NULL,
`value` VARCHAR(255) NOT NULL,
PRIMARY KEY (`key_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Default settings
INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES
('max_length', '15'),
('min_rank_to_buy', '1'),
('price_credits', '5'),
('price_points', '0'),
('points_type', '0');
-- 3. Blacklisted words table
-- ------------------------------------------------------------
-- 5. Blacklist table
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`word` VARCHAR(100) NOT NULL,
@@ -34,13 +73,249 @@ CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
UNIQUE KEY `uk_word` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Example blacklist entries (customize as needed)
-- ============================================================
-- Schema upgrades for existing installations
-- ============================================================
-- ------------------------------------------------------------
-- user_prefixes: add missing columns safely
-- ------------------------------------------------------------
SET @dbname = DATABASE();
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'font'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `font` VARCHAR(50) NOT NULL DEFAULT '''' AFTER `effect`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'catalog_prefix_id'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `catalog_prefix_id` INT(11) NOT NULL DEFAULT 0 AFTER `font`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'display_name'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `display_name` VARCHAR(100) NOT NULL DEFAULT '''' AFTER `catalog_prefix_id`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'points'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `points` INT(11) NOT NULL DEFAULT 0 AFTER `display_name`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'points_type'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `points_type` INT(11) NOT NULL DEFAULT 0 AFTER `points`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'user_prefixes'
AND COLUMN_NAME = 'is_custom'
),
'SELECT 1',
'ALTER TABLE `user_prefixes` ADD COLUMN `is_custom` TINYINT(1) NOT NULL DEFAULT 1 AFTER `points_type`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- ------------------------------------------------------------
-- custom_prefixes_catalog: add missing columns safely
-- ------------------------------------------------------------
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'font'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `font` VARCHAR(50) NOT NULL DEFAULT '''' AFTER `effect`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'points'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `points` INT(11) NOT NULL DEFAULT 0 AFTER `font`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'points_type'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `points_type` INT(11) NOT NULL DEFAULT 0 AFTER `points`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'enabled'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `enabled` TINYINT(1) NOT NULL DEFAULT 1 AFTER `points_type`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'custom_prefixes_catalog'
AND COLUMN_NAME = 'sort_order'
),
'SELECT 1',
'ALTER TABLE `custom_prefixes_catalog` ADD COLUMN `sort_order` INT(11) NOT NULL DEFAULT 0 AFTER `enabled`'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- ============================================================
-- Default settings
-- ============================================================
INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES
('max_length', '15'),
('min_rank_to_buy', '1'),
('price_credits', '5'),
('price_points', '0'),
('points_type', '0'),
('font_price_credits', '10'),
('font_price_points', '0'),
('font_points_type', '0');
-- ============================================================
-- Default catalog entries
-- ============================================================
INSERT IGNORE INTO `custom_prefixes_catalog`
(`id`, `display_name`, `text`, `color`, `icon`, `effect`, `font`, `points`, `points_type`, `enabled`, `sort_order`) VALUES
(1, 'VIP', 'VIP', '#FFD700', '', 'glow', '', 10, 0, 1, 1),
(2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2),
(3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3);
-- ============================================================
-- Example blacklist entries
-- ============================================================
INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES
('admin'),
('staff'),
('mod'),
('owner');
-- ============================================================
-- Notes
-- ============================================================
-- Preset prefixes for `:customize` are loaded directly by
-- UserNickIconsComposer and displayed inside the `:customize` panel.
--
-- This setup does not require rows in `catalog_pages`.
--
-- Command texts / permission inserts are intentionally omitted
-- for compatibility with both legacy and normalized permission schemas.
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
-- GivePrefix command
('commands.keys.cmd_give_prefix', 'giveprefix'),
@@ -79,4 +354,3 @@ VALUES
('cmd_list_prefixes', '1', '0', '0', '0', '0', '0', '0', '1'),
('cmd_remove_prefix', '1', '0', '0', '0', '0', '0', '0', '1'),
('cmd_prefix_blacklist', '1', '0', '0', '0', '0', '0', '0', '1');
@@ -0,0 +1,244 @@
CREATE TABLE IF NOT EXISTS `infostand_backgrounds` (
`id` int(11) NOT NULL,
`category` enum('background','stand','overlay','card') NOT NULL,
`min_rank` int(11) NOT NULL DEFAULT 0,
`is_hc_only` tinyint(1) NOT NULL DEFAULT 0,
`is_ambassador_only` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`,`category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT INTO `infostand_backgrounds` (`id`, `category`, `min_rank`, `is_hc_only`, `is_ambassador_only`) VALUES
(0, 'background', 0, 0, 0),
(1, 'background', 0, 0, 0),
(2, 'background', 0, 0, 0),
(3, 'background', 0, 0, 0),
(4, 'background', 0, 0, 0),
(5, 'background', 0, 0, 0),
(6, 'background', 0, 0, 0),
(7, 'background', 0, 0, 0),
(8, 'background', 0, 0, 0),
(9, 'background', 0, 0, 0),
(10, 'background', 0, 0, 0),
(11, 'background', 0, 0, 0),
(12, 'background', 0, 0, 0),
(13, 'background', 0, 0, 0),
(14, 'background', 0, 0, 0),
(15, 'background', 0, 0, 0),
(16, 'background', 0, 0, 0),
(17, 'background', 0, 0, 0),
(18, 'background', 0, 0, 0),
(19, 'background', 0, 0, 0),
(20, 'background', 0, 0, 0),
(21, 'background', 0, 0, 0),
(22, 'background', 0, 0, 0),
(23, 'background', 0, 0, 0),
(24, 'background', 0, 0, 0),
(25, 'background', 0, 0, 0),
(26, 'background', 0, 0, 0),
(27, 'background', 0, 0, 0),
(28, 'background', 0, 0, 0),
(29, 'background', 0, 0, 0),
(30, 'background', 0, 0, 0),
(31, 'background', 0, 0, 0),
(32, 'background', 0, 0, 0),
(33, 'background', 0, 0, 0),
(34, 'background', 0, 0, 0),
(35, 'background', 0, 0, 0),
(36, 'background', 0, 0, 0),
(37, 'background', 0, 0, 0),
(38, 'background', 0, 0, 0),
(39, 'background', 0, 0, 0),
(40, 'background', 0, 0, 0),
(41, 'background', 0, 0, 0),
(42, 'background', 0, 1, 0),
(43, 'background', 0, 1, 0),
(44, 'background', 0, 1, 0),
(45, 'background', 0, 1, 0),
(46, 'background', 0, 1, 0),
(47, 'background', 0, 1, 0),
(48, 'background', 0, 1, 0),
(49, 'background', 0, 1, 0),
(50, 'background', 0, 1, 0),
(51, 'background', 0, 1, 0),
(52, 'background', 0, 1, 0),
(53, 'background', 0, 1, 0),
(54, 'background', 0, 1, 0),
(55, 'background', 0, 1, 0),
(56, 'background', 0, 1, 0),
(57, 'background', 0, 1, 0),
(58, 'background', 0, 1, 0),
(59, 'background', 0, 1, 0),
(60, 'background', 0, 1, 0),
(61, 'background', 0, 1, 0),
(62, 'background', 0, 1, 0),
(63, 'background', 0, 1, 0),
(64, 'background', 0, 1, 0),
(65, 'background', 0, 1, 0),
(66, 'background', 0, 1, 0),
(67, 'background', 0, 1, 0),
(68, 'background', 0, 1, 0),
(69, 'background', 0, 1, 0),
(70, 'background', 0, 1, 0),
(71, 'background', 0, 1, 0),
(72, 'background', 0, 1, 0),
(73, 'background', 0, 1, 0),
(74, 'background', 0, 1, 0),
(75, 'background', 0, 1, 0),
(76, 'background', 0, 1, 0),
(77, 'background', 0, 1, 0),
(78, 'background', 0, 1, 0),
(79, 'background', 0, 1, 0),
(80, 'background', 0, 1, 0),
(81, 'background', 0, 1, 0),
(82, 'background', 0, 1, 0),
(83, 'background', 0, 1, 0),
(84, 'background', 0, 1, 0),
(85, 'background', 0, 1, 0),
(86, 'background', 0, 1, 0),
(87, 'background', 0, 1, 0),
(88, 'background', 0, 1, 0),
(89, 'background', 0, 1, 0),
(90, 'background', 0, 1, 0),
(91, 'background', 0, 1, 0),
(92, 'background', 0, 1, 0),
(93, 'background', 0, 1, 0),
(94, 'background', 0, 1, 0),
(95, 'background', 0, 1, 0),
(96, 'background', 0, 1, 0),
(97, 'background', 0, 1, 0),
(98, 'background', 0, 1, 0),
(99, 'background', 0, 1, 0),
(100, 'background', 0, 1, 0),
(101, 'background', 2, 0, 0),
(102, 'background', 0, 1, 0),
(103, 'background', 0, 1, 0),
(104, 'background', 0, 1, 0),
(105, 'background', 0, 1, 0),
(106, 'background', 0, 1, 0),
(107, 'background', 0, 1, 0),
(108, 'background', 0, 1, 0),
(109, 'background', 0, 1, 0),
(110, 'background', 0, 1, 0),
(111, 'background', 0, 1, 0),
(112, 'background', 0, 1, 0),
(113, 'background', 0, 1, 0),
(114, 'background', 0, 1, 0),
(115, 'background', 0, 1, 0),
(116, 'background', 0, 1, 0),
(117, 'background', 0, 1, 0),
(118, 'background', 0, 1, 0),
(119, 'background', 0, 1, 0),
(120, 'background', 0, 1, 0),
(121, 'background', 0, 1, 0),
(122, 'background', 0, 1, 0),
(123, 'background', 0, 1, 0),
(124, 'background', 0, 1, 0),
(125, 'background', 0, 1, 0),
(126, 'background', 0, 1, 0),
(127, 'background', 0, 1, 0),
(128, 'background', 0, 1, 0),
(129, 'background', 0, 1, 0),
(130, 'background', 0, 1, 0),
(131, 'background', 0, 1, 0),
(132, 'background', 0, 1, 0),
(133, 'background', 0, 1, 0),
(134, 'background', 0, 1, 0),
(135, 'background', 0, 1, 0),
(136, 'background', 0, 1, 0),
(137, 'background', 0, 1, 0),
(138, 'background', 0, 1, 0),
(139, 'background', 0, 1, 0),
(140, 'background', 0, 1, 0),
(141, 'background', 0, 1, 0),
(142, 'background', 0, 1, 0),
(143, 'background', 0, 1, 0),
(144, 'background', 0, 1, 0),
(145, 'background', 0, 1, 0),
(146, 'background', 0, 1, 0),
(147, 'background', 0, 1, 0),
(148, 'background', 0, 1, 0),
(149, 'background', 0, 1, 0),
(150, 'background', 0, 1, 0),
(151, 'background', 0, 1, 0),
(152, 'background', 0, 1, 0),
(153, 'background', 0, 1, 0),
(154, 'background', 0, 1, 0),
(155, 'background', 0, 1, 0),
(156, 'background', 0, 1, 0),
(157, 'background', 0, 1, 0),
(158, 'background', 0, 1, 0),
(159, 'background', 0, 1, 0),
(160, 'background', 0, 1, 0),
(161, 'background', 0, 1, 0),
(162, 'background', 0, 1, 0),
(163, 'background', 0, 1, 0),
(164, 'background', 0, 1, 0),
(165, 'background', 0, 1, 0),
(166, 'background', 0, 1, 0),
(167, 'background', 0, 1, 0),
(168, 'background', 0, 1, 0),
(169, 'background', 0, 1, 0),
(170, 'background', 0, 1, 0),
(171, 'background', 0, 1, 0),
(172, 'background', 0, 1, 0),
(173, 'background', 0, 1, 0),
(174, 'background', 0, 1, 0),
(175, 'background', 0, 1, 0),
(176, 'background', 0, 1, 0),
(177, 'background', 0, 1, 0),
(178, 'background', 0, 1, 0),
(179, 'background', 0, 1, 0),
(180, 'background', 0, 1, 0),
(181, 'background', 0, 1, 0),
(182, 'background', 0, 1, 0),
(183, 'background', 0, 1, 0),
(184, 'background', 0, 1, 0),
(185, 'background', 0, 1, 0),
(186, 'background', 0, 1, 0),
(187, 'background', 0, 1, 0),
(0, 'stand', 0, 0, 0),
(1, 'stand', 0, 0, 0),
(2, 'stand', 0, 0, 0),
(3, 'stand', 0, 0, 0),
(4, 'stand', 0, 0, 0),
(5, 'stand', 0, 0, 0),
(6, 'stand', 0, 0, 0),
(7, 'stand', 0, 0, 0),
(8, 'stand', 0, 0, 0),
(9, 'stand', 0, 0, 0),
(10, 'stand', 0, 0, 0),
(11, 'stand', 0, 0, 0),
(12, 'stand', 0, 0, 0),
(13, 'stand', 0, 0, 0),
(14, 'stand', 0, 0, 0),
(15, 'stand', 0, 0, 0),
(16, 'stand', 0, 1, 0),
(17, 'stand', 0, 1, 0),
(18, 'stand', 0, 1, 0),
(19, 'stand', 0, 1, 0),
(20, 'stand', 0, 1, 0),
(21, 'stand', 0, 1, 0),
(0, 'overlay', 0, 0, 0),
(1, 'overlay', 0, 0, 0),
(2, 'overlay', 0, 1, 0),
(3, 'overlay', 0, 1, 0),
(4, 'overlay', 0, 1, 0),
(5, 'overlay', 0, 1, 0),
(6, 'overlay', 0, 1, 0),
(7, 'overlay', 0, 1, 0),
(8, 'overlay', 0, 1, 0),
(1, 'card', 0, 0, 0),
(2, 'card', 0, 0, 0),
(3, 'card', 0, 0, 0),
(4, 'card', 0, 0, 0),
(5, 'card', 0, 0, 0),
(6, 'card', 0, 0, 0),
(7, 'card', 0, 0, 0),
(8, 'card', 0, 0, 0),
(9, 'card', 0, 0, 0),
(10, 'card', 0, 0, 0),
(11, 'card', 0, 0, 0),
(12, 'card', 0, 0, 0),
(13, 'card', 0, 0, 0),
(14, 'card', 0, 0, 0),
(15, 'card', 0, 0, 0);
@@ -0,0 +1,5 @@
ALTER TABLE users
ADD COLUMN `last_username_change` INT(11) NOT NULL;
INSERT INTO emulator_settings (`key`, `value`, `comment`)
VALUES ('rename.cooldown_days', '30', 'Days between username changes');
@@ -0,0 +1,51 @@
-- ============================================================
-- 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`;
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`);
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>
<artifactId>Habbo</artifactId>
<version>4.1.12</version>
<version>4.1.15</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -163,7 +163,7 @@
<version>2.13.0</version>
</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 -->
<dependency>
<groupId>org.mindrot</groupId>
@@ -171,7 +171,7 @@
<version>0.4</version>
</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 -->
<dependency>
<groupId>org.eclipse.angus</groupId>
@@ -21,8 +21,10 @@ import com.eu.habbo.habbohotel.pets.PetManager;
import com.eu.habbo.habbohotel.polls.PollManager;
import com.eu.habbo.habbohotel.rooms.RoomChatBubbleManager;
import com.eu.habbo.habbohotel.rooms.RoomManager;
import com.eu.habbo.habbohotel.translations.GoogleTranslateManager;
import com.eu.habbo.habbohotel.users.HabboManager;
import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeManager;
import com.eu.habbo.habbohotel.users.infostand.InfostandBackgroundManager;
import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionManager;
import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionScheduler;
import org.slf4j.Logger;
@@ -59,7 +61,9 @@ public class GameEnvironment {
private SubscriptionManager subscriptionManager;
private CalendarManager calendarManager;
private RoomChatBubbleManager roomChatBubbleManager;
private GoogleTranslateManager googleTranslateManager;
private CustomBadgeManager customBadgeManager;
private InfostandBackgroundManager infostandBackgroundManager;
public void load() throws Exception {
LOGGER.info("GameEnvironment -> Loading...");
@@ -86,7 +90,9 @@ public class GameEnvironment {
this.pollManager = new PollManager();
this.calendarManager = new CalendarManager();
this.roomChatBubbleManager = new RoomChatBubbleManager();
this.googleTranslateManager = new GoogleTranslateManager();
this.customBadgeManager = new CustomBadgeManager();
this.infostandBackgroundManager = new InfostandBackgroundManager();
this.roomManager.loadPublicRooms();
this.navigatorManager.loadNavigator();
@@ -124,6 +130,9 @@ public class GameEnvironment {
this.hotelViewManager.dispose();
this.subscriptionManager.dispose();
this.calendarManager.dispose();
if (this.googleTranslateManager != null) {
this.googleTranslateManager.clearCache();
}
LOGGER.info("GameEnvironment -> Disposed!");
}
@@ -223,7 +232,15 @@ public class GameEnvironment {
return roomChatBubbleManager;
}
public GoogleTranslateManager getGoogleTranslateManager() {
return this.googleTranslateManager;
}
public CustomBadgeManager getCustomBadgeManager() {
return this.customBadgeManager;
}
public InfostandBackgroundManager getInfostandBackgroundManager() {
return this.infostandBackgroundManager;
}
}
@@ -1,7 +1,6 @@
package com.eu.habbo.habbohotel.achievements;
import com.eu.habbo.Emulator;
import com.eu.habbo.database.SqlQueries;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboBadge;
@@ -50,12 +49,16 @@ public class AchievementManager {
if (habbo != null) {
progressAchievement(habbo, achievement, amount);
} else {
try {
SqlQueries.update(
"INSERT INTO users_achievements_queue (user_id, achievement_id, amount) VALUES (?, ?, ?) "
+ "ON DUPLICATE KEY UPDATE amount = amount + ?",
habboId, achievement.id, amount, amount);
} catch (SqlQueries.DataAccessException e) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("" +
"INSERT INTO users_achievements_queue (user_id, achievement_id, amount) VALUES (?, ?, ?) " +
"ON DUPLICATE KEY UPDATE amount = amount + ?")) {
statement.setInt(1, habboId);
statement.setInt(2, achievement.id);
statement.setInt(3, amount);
statement.setInt(4, amount);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
@@ -200,41 +203,48 @@ public class AchievementManager {
}
public static void createUserEntry(Habbo habbo, Achievement achievement) {
try {
SqlQueries.update(
"INSERT INTO users_achievements (user_id, achievement_name, progress) VALUES (?, ?, ?)",
habbo.getHabboInfo().getId(), achievement.name, 1);
} catch (SqlQueries.DataAccessException e) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_achievements (user_id, achievement_name, progress) VALUES (?, ?, ?)")) {
statement.setInt(1, habbo.getHabboInfo().getId());
statement.setString(2, achievement.name);
statement.setInt(3, 1);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
public static void saveAchievements(Habbo habbo) {
int userId = habbo.getHabboInfo().getId();
try {
SqlQueries.batchUpdate(
"UPDATE users_achievements SET progress = ? WHERE achievement_name = ? AND user_id = ? LIMIT 1",
habbo.getHabboStats().getAchievementProgress().entrySet(),
(ps, entry) -> {
ps.setInt(1, entry.getValue());
ps.setString(2, entry.getKey().name);
ps.setInt(3, userId);
});
} catch (SqlQueries.DataAccessException e) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users_achievements SET progress = ? WHERE achievement_name = ? AND user_id = ? LIMIT 1")) {
statement.setInt(3, habbo.getHabboInfo().getId());
for (Map.Entry<Achievement, Integer> map : habbo.getHabboStats().getAchievementProgress().entrySet()) {
statement.setInt(1, map.getValue());
statement.setString(2, map.getKey().name);
statement.addBatch();
}
statement.executeBatch();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
public static int getAchievementProgressForHabbo(int userId, Achievement achievement) {
try {
return SqlQueries.queryOne(
"SELECT progress FROM users_achievements WHERE user_id = ? AND achievement_name = ? LIMIT 1",
rs -> rs.getInt("progress"),
userId, achievement.name).orElse(0);
} catch (SqlQueries.DataAccessException e) {
LOGGER.error("Caught SQL exception", e);
if (achievement == null) {
return 0;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT progress FROM users_achievements WHERE user_id = ? AND achievement_name = ? LIMIT 1")) {
statement.setInt(1, userId);
statement.setString(2, achievement.name);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) {
return set.getInt("progress");
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
return 0;
}
public void reload() {
@@ -71,13 +71,23 @@ public class BotManager {
}
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;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO bots (user_id, room_id, name, motto, figure, gender, type) VALUES (0, 0, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) {
statement.setString(1, data.get("name"));
statement.setString(2, data.get("motto"));
statement.setString(3, data.get("figure"));
statement.setString(4, data.get("gender").toUpperCase());
statement.setString(5, type);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO bots (user_id, room_id, name, motto, figure, gender, type) VALUES (?, 0, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, ownerId);
statement.setString(2, data.get("name"));
statement.setString(3, data.get("motto"));
statement.setString(4, data.get("figure"));
statement.setString(5, data.get("gender").toUpperCase());
statement.setString(6, type);
statement.execute();
try (ResultSet set = statement.getGeneratedKeys()) {
if (set.next()) {
@@ -1058,7 +1058,7 @@ public class CatalogManager {
}
}
Bot bot = Emulator.getGameEnvironment().getBotManager().createBot(data, type);
Bot bot = Emulator.getGameEnvironment().getBotManager().createBot(data, type, habbo.getHabboInfo().getId());
if (bot != null) {
bot.setOwnerId(habbo.getClient().getHabbo().getHabboInfo().getId());
@@ -1,18 +0,0 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.messages.outgoing.users.UserDataComposer;
public class ChangeNameCommand extends Command {
public ChangeNameCommand() {
super("cmd_changename", Emulator.getTexts().getValue("commands.keys.cmd_changename").split(";"));
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
gameClient.getHabbo().getHabboStats().allowNameChange = !gameClient.getHabbo().getHabboStats().allowNameChange;
gameClient.sendResponse(new UserDataComposer(gameClient.getHabbo()));
return true;
}
}
@@ -184,7 +184,6 @@ public class CommandHandler {
addCommand(new BlockAlertCommand());
addCommand(new BotsCommand());
addCommand(new CalendarCommand());
addCommand(new ChangeNameCommand());
addCommand(new ChatTypeCommand());
addCommand(new CommandsCommand());
addCommand(new ControlCommand());
@@ -2,11 +2,13 @@ package com.eu.habbo.habbohotel.guilds;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.guilds.forums.ForumThread;
import com.eu.habbo.habbohotel.guilds.forums.ForumView;
import com.eu.habbo.habbohotel.items.interactions.InteractionGuildFurni;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.outgoing.guilds.GuildJoinErrorComposer;
import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer;
import gnu.trove.TCollections;
import gnu.trove.iterator.TIntObjectIterator;
import gnu.trove.map.TIntObjectMap;
@@ -142,12 +144,36 @@ public class GuildManager {
deleteFavourite.execute();
}
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM guild_forum_views WHERE guild_id = ?")) {
statement.setInt(1, guild.getId());
statement.execute();
}
try (PreparedStatement statement = connection.prepareStatement("DELETE c FROM guilds_forums_comments c INNER JOIN guilds_forums_threads t ON c.thread_id = t.id WHERE t.guild_id = ?")) {
statement.setInt(1, guild.getId());
statement.execute();
}
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM guilds_forums_threads WHERE guild_id = ?")) {
statement.setInt(1, guild.getId());
statement.execute();
}
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM guilds_members WHERE guild_id = ?")) {
statement.setInt(1, guild.getId());
statement.execute();
}
try (PreparedStatement statement = connection.prepareStatement("UPDATE rooms SET guild_id = 0 WHERE guild_id = ?")) {
statement.setInt(1, guild.getId());
statement.execute();
}
try (PreparedStatement statement = connection.prepareStatement("UPDATE items SET guild_id = 0 WHERE guild_id = ?")) {
statement.setInt(1, guild.getId());
statement.execute();
}
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM guilds WHERE id = ?")) {
statement.setInt(1, guild.getId());
statement.execute();
@@ -161,6 +187,10 @@ public class GuildManager {
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
this.guilds.remove(guild.getId());
ForumThread.clearCacheForGuild(guild.getId());
GuildForumDataComposer.invalidateUnreadCache(guild.getId());
}
@@ -1,5 +1,6 @@
package com.eu.habbo.habbohotel.items.interactions.wired.conditions;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
@@ -11,10 +12,12 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.messages.ServerMessage;
import gnu.trove.set.hash.THashSet;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.stream.Collectors;
public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private static final int COMPARISON_LESS_THAN = 0;
@@ -23,9 +26,16 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private static final int SOURCE_GROUP_USERS = 0;
private static final int SOURCE_GROUP_FURNI = 1;
private static final int SOURCE_USER_TRIGGER = 0;
private static final int SOURCE_USER_SIGNAL = 1;
private static final int SOURCE_USER_CLICKED = 2;
private static final int SOURCE_FURNI_TRIGGER = 3;
private static final int SOURCE_FURNI_PICKED = 4;
private static final int SOURCE_FURNI_SIGNAL = 5;
public static final WiredConditionType type = WiredConditionType.SLC_QUANTITY;
private final THashSet<HabboItem> items;
private int comparison = COMPARISON_EQUAL;
private int quantity = 0;
private int sourceGroup = SOURCE_GROUP_USERS;
@@ -33,10 +43,12 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
public WiredConditionSelectionQuantity(ResultSet set, Item baseItem) throws SQLException {
super(set, baseItem);
this.items = new THashSet<>();
}
public WiredConditionSelectionQuantity(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
super(id, userId, item, extradata, limitedStack, limitedSells);
this.items = new THashSet<>();
}
@Override
@@ -46,9 +58,18 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
@Override
public void serializeWiredData(ServerMessage message, Room room) {
message.appendBoolean(false);
message.appendInt(5);
message.appendInt(0);
this.refresh(room);
boolean pickMode = this.sourceGroup == SOURCE_GROUP_FURNI && this.sourceType == WiredSourceUtil.SOURCE_SELECTED;
message.appendBoolean(pickMode);
message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION);
message.appendInt(pickMode ? this.items.size() : 0);
if (pickMode) {
for (HabboItem item : this.items) {
message.appendInt(item.getId());
}
}
message.appendInt(this.getBaseItem().getSpriteId());
message.appendInt(this.getId());
message.appendString("");
@@ -69,8 +90,36 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL;
this.quantity = (params.length > 1) ? this.normalizeQuantity(params[1]) : 0;
this.sourceGroup = (params.length > 2) ? this.normalizeSourceGroup(params[2]) : SOURCE_GROUP_USERS;
this.sourceType = (params.length > 3) ? this.normalizeSourceType(this.sourceGroup, params[3]) : WiredSourceUtil.SOURCE_TRIGGER;
this.items.clear();
if (params.length > 3) {
this.sourceGroup = this.normalizeSourceGroup(params[2]);
this.sourceType = this.normalizeSourceType(this.sourceGroup, params[3]);
} else {
this.setSourceSelection((params.length > 2) ? params[2] : SOURCE_USER_TRIGGER);
}
if (this.sourceGroup != SOURCE_GROUP_FURNI || this.sourceType != WiredSourceUtil.SOURCE_SELECTED) {
return true;
}
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId());
if (room == null) {
return false;
}
int count = settings.getFurniIds().length;
if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) {
return false;
}
for (int itemId : settings.getFurniIds()) {
HabboItem item = room.getHabboItem(itemId);
if (item != null) {
this.items.add(item);
}
}
return true;
}
@@ -97,11 +146,14 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
@Override
public String getWiredData() {
this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()));
return WiredManager.getGson().toJson(new JsonData(
this.comparison,
this.quantity,
this.sourceGroup,
this.sourceType
this.sourceType,
this.items.stream().map(HabboItem::getId).collect(Collectors.toList())
));
}
@@ -125,6 +177,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
this.quantity = this.normalizeQuantity(data.quantity);
this.sourceGroup = this.normalizeSourceGroup(data.sourceGroup);
this.sourceType = this.normalizeSourceType(this.sourceGroup, data.sourceType);
this.loadSelectedItems(data.itemIds, room);
return;
}
@@ -150,6 +203,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
@Override
public void onPickUp() {
this.items.clear();
this.comparison = COMPARISON_EQUAL;
this.quantity = 0;
this.sourceGroup = SOURCE_GROUP_USERS;
@@ -158,7 +212,7 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private int resolveCount(WiredContext ctx) {
if (this.sourceGroup == SOURCE_GROUP_FURNI) {
List<HabboItem> items = WiredSourceUtil.resolveItems(ctx, this.sourceType, null);
List<HabboItem> items = WiredSourceUtil.resolveItems(ctx, this.sourceType, this.items);
return items.size();
}
@@ -188,10 +242,18 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
private int normalizeSourceType(int group, int value) {
if (group == SOURCE_GROUP_USERS) {
return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER;
switch (value) {
case WiredSourceUtil.SOURCE_CLICKED_USER:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_SELECTOR:
return value;
default:
return WiredSourceUtil.SOURCE_TRIGGER;
}
}
switch (value) {
case WiredSourceUtil.SOURCE_SELECTED:
case WiredSourceUtil.SOURCE_SELECTOR:
case WiredSourceUtil.SOURCE_SIGNAL:
case WiredSourceUtil.SOURCE_TRIGGER:
@@ -201,17 +263,104 @@ public class WiredConditionSelectionQuantity extends InteractionWiredCondition {
}
}
private int getSourceSelection() {
if (this.sourceGroup == SOURCE_GROUP_FURNI) {
switch (this.sourceType) {
case WiredSourceUtil.SOURCE_SELECTED:
return SOURCE_FURNI_PICKED;
case WiredSourceUtil.SOURCE_SIGNAL:
return SOURCE_FURNI_SIGNAL;
default:
return SOURCE_FURNI_TRIGGER;
}
}
switch (this.sourceType) {
case WiredSourceUtil.SOURCE_CLICKED_USER:
return SOURCE_USER_CLICKED;
case WiredSourceUtil.SOURCE_SIGNAL:
return SOURCE_USER_SIGNAL;
default:
return SOURCE_USER_TRIGGER;
}
}
private void setSourceSelection(int value) {
switch (value) {
case SOURCE_USER_SIGNAL:
this.sourceGroup = SOURCE_GROUP_USERS;
this.sourceType = WiredSourceUtil.SOURCE_SIGNAL;
break;
case SOURCE_USER_CLICKED:
this.sourceGroup = SOURCE_GROUP_USERS;
this.sourceType = WiredSourceUtil.SOURCE_CLICKED_USER;
break;
case SOURCE_FURNI_TRIGGER:
this.sourceGroup = SOURCE_GROUP_FURNI;
this.sourceType = WiredSourceUtil.SOURCE_TRIGGER;
break;
case SOURCE_FURNI_PICKED:
this.sourceGroup = SOURCE_GROUP_FURNI;
this.sourceType = WiredSourceUtil.SOURCE_SELECTED;
break;
case SOURCE_FURNI_SIGNAL:
this.sourceGroup = SOURCE_GROUP_FURNI;
this.sourceType = WiredSourceUtil.SOURCE_SIGNAL;
break;
default:
this.sourceGroup = SOURCE_GROUP_USERS;
this.sourceType = WiredSourceUtil.SOURCE_TRIGGER;
break;
}
}
private void loadSelectedItems(List<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 {
int comparison;
int quantity;
int sourceGroup;
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.quantity = quantity;
this.sourceGroup = sourceGroup;
this.sourceType = sourceType;
this.itemIds = itemIds;
}
}
}
@@ -82,7 +82,7 @@ public class WiredEffectBotTalk extends InteractionWiredEffect {
this.setDelay(delay);
this.botName = data[0].substring(0, Math.min(data[0].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.bot.message.max_length", 100)));
this.mode = mode;
return true;
@@ -105,7 +105,7 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect {
throw new WiredSaveException("Delay too long");
this.botName = data[0].substring(0, Math.min(data[0].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
this.message = data[1].substring(0, Math.min(data[1].length(), Emulator.getConfig().getInt("hotel.wired.bot.message.max_length", 100)));
this.mode = mode;
this.setDelay(delay);
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect;
import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameTimer;
import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameUpCounter;
import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings;
import com.eu.habbo.habbohotel.rooms.Room;
@@ -60,29 +61,74 @@ public class WiredEffectControlClock extends InteractionWiredEffect {
}
for (HabboItem item : effectiveItems) {
if (!(item instanceof InteractionGameUpCounter)) {
if (!(item instanceof InteractionGameTimer)) {
continue;
}
InteractionGameUpCounter counter = (InteractionGameUpCounter) item;
switch (this.action) {
case ACTION_START:
counter.restartFromZero(room);
break;
case ACTION_STOP:
counter.stopCounter(room);
break;
case ACTION_RESET:
counter.resetCounter(room);
break;
case ACTION_PAUSE:
counter.pauseCounter(room);
break;
case ACTION_RESUME:
counter.resumeCounter(room);
break;
if (item instanceof InteractionGameUpCounter) {
this.controlUpCounter((InteractionGameUpCounter) item, room);
continue;
}
this.controlGameTimer((InteractionGameTimer) item, room);
}
}
private void controlUpCounter(InteractionGameUpCounter counter, Room room) {
switch (this.action) {
case ACTION_START:
counter.restartFromZero(room);
break;
case ACTION_STOP:
counter.stopCounter(room);
break;
case ACTION_RESET:
counter.resetCounter(room);
break;
case ACTION_PAUSE:
counter.pauseCounter(room);
break;
case ACTION_RESUME:
counter.resumeCounter(room);
break;
}
}
private void controlGameTimer(InteractionGameTimer timer, Room room) {
switch (this.action) {
case ACTION_START:
timer.startTimer(room);
break;
case ACTION_STOP:
this.stopGameTimer(timer, room, false);
break;
case ACTION_RESET:
this.stopGameTimer(timer, room, true);
break;
case ACTION_PAUSE:
timer.pauseTimer(room);
break;
case ACTION_RESUME:
timer.resumeTimer(room);
break;
}
}
private void stopGameTimer(InteractionGameTimer timer, Room room, boolean resetTime) {
boolean wasActive = timer.isRunning() || timer.isPaused();
timer.endGame(room);
if (resetTime) {
timer.setTimeNow(timer.getBaseTime());
timer.setExtradata(timer.getTimeNow() + "\t" + timer.getBaseTime());
}
room.updateItem(timer);
timer.needsUpdate(true);
if (wasActive) {
WiredManager.triggerGameEnds(room);
}
}
@@ -206,7 +252,7 @@ public class WiredEffectControlClock extends InteractionWiredEffect {
throw new WiredSaveException(String.format("Item %s not found", itemId));
}
if (!(item instanceof InteractionGameUpCounter)) {
if (!(item instanceof InteractionGameTimer)) {
throw new WiredSaveException("wiredfurni.error.require_counter_furni");
}
@@ -53,26 +53,37 @@ public class WiredEffectFurniToFurni extends InteractionWiredEffect {
return;
}
HabboItem moveItem = this.resolveLastMoveItem(ctx);
HabboItem targetItem = this.resolveLastTargetItem(ctx);
List<HabboItem> moveItems = this.resolveMoveItems(ctx);
List<HabboItem> targetItems = this.resolveTargetItems(ctx);
if (moveItem == null || targetItem == null || moveItem.getId() == targetItem.getId()) {
if (moveItems.isEmpty() || targetItems.isEmpty()) {
return;
}
RoomTile targetTile = room.getLayout().getTile(targetItem.getX(), targetItem.getY());
if (targetTile == null) {
return;
}
int targetIndex = 0;
for (HabboItem moveItem : moveItems) {
if (moveItem == null) {
continue;
}
FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), null, false, ctx);
if (error == FurnitureMovementError.NONE) {
return;
}
HabboItem targetItem = targetItems.get(targetIndex % targetItems.size());
targetIndex++;
error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), targetItem.getZ(), null, false, ctx);
if (error == FurnitureMovementError.NONE) {
return;
if (targetItem == null || moveItem.getId() == targetItem.getId()) {
continue;
}
RoomTile targetTile = room.getLayout().getTile(targetItem.getX(), targetItem.getY());
if (targetTile == null) {
continue;
}
FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), null, false, ctx);
if (error == FurnitureMovementError.NONE) {
continue;
}
WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), targetItem.getZ(), null, false, ctx);
}
}
@@ -233,35 +244,23 @@ public class WiredEffectFurniToFurni extends InteractionWiredEffect {
return COOLDOWN_MOVEMENT;
}
private HabboItem resolveLastMoveItem(WiredContext ctx) {
return this.resolveLastItem(ctx, this.moveSource, this.moveItems);
private List<HabboItem> resolveMoveItems(WiredContext ctx) {
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;
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) {
this.validateItems(items);
}
List<HabboItem> resolvedItems = WiredSourceUtil.resolveItems(ctx, source, items);
if (resolvedItems.isEmpty()) {
return null;
}
for (int index = resolvedItems.size() - 1; index >= 0; index--) {
HabboItem item = resolvedItems.get(index);
if (item != null) {
return item;
}
}
return null;
return WiredSourceUtil.resolveItems(ctx, source, items).stream()
.filter(item -> item != null && ctx.room().getHabboItem(item.getId()) != null)
.collect(Collectors.toList());
}
private List<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;
private static final int MAX_SIGNAL_DEPTH = 10;
public static int MAX_SIGNAL_DEPTH = 100;
private static final int ANTENNA_PICKED = 0;
private static final int ANTENNA_TRIGGER = 1;
@@ -166,7 +166,7 @@ public class WiredEffectSendSignal extends InteractionWiredEffect {
.signalChannel(signalChannel)
.signalUserCount(signalUserCount)
.signalFurniCount(sourceItem != null ? 1 : 0)
.contextVariableScope(ctx.contextVariables())
.contextVariableScope(ctx.contextVariables().copy())
.triggeredByEffect(true);
if (actor != null) builder.actor(actor);
@@ -286,15 +286,6 @@ public class WiredEffectSendSignal extends InteractionWiredEffect {
}
}
if (room != null && room.getRoomSpecialTypes() != null) {
for (HabboItem receiver : newItems) {
int count = room.getRoomSpecialTypes().countSendersTargetingReceiver(receiver.getId(), this);
if (count >= RoomSpecialTypes.MAX_SENDERS_PER_RECEIVER) {
throw new WiredSaveException("Maximum of " + RoomSpecialTypes.MAX_SENDERS_PER_RECEIVER + " senders per receiver reached");
}
}
}
int delay = settings.getDelay();
if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) {
throw new WiredSaveException("Delay too long");
@@ -34,6 +34,8 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
private static final long DELIVERY_DEDUP_TTL_MS = 60_000L;
private static final int DELIVERY_DEDUP_CLEANUP_THRESHOLD = 512;
private static final ConcurrentHashMap<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 int userSource = WiredSourceUtil.SOURCE_TRIGGER;
@@ -96,9 +98,12 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
if(gameClient.getHabbo() == null || !gameClient.getHabbo().hasPermission(Permission.ACC_SUPERWIRED)) {
message = Emulator.getGameEnvironment().getWordFilter().filter(message, null);
message = message.substring(0, Math.min(message.length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100)));
}
int maxLength = Emulator.getConfig().getInt("hotel.wired.show_message.max_length", DEFAULT_SHOW_MESSAGE_MAX_LENGTH);
int maxLines = Emulator.getConfig().getInt("hotel.wired.show_message.max_lines", DEFAULT_SHOW_MESSAGE_MAX_LINES);
message = clampMessage(message, maxLength, maxLines);
int delay = settings.getDelay();
if(delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20))
@@ -109,6 +114,35 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
return true;
}
private static String clampMessage(String value, int maxLength, int maxLines) {
if (value == null || value.isEmpty()) {
return "";
}
int safeMaxLength = Math.max(1, maxLength);
int safeMaxLines = Math.max(1, maxLines);
String normalized = value.replace("\r\n", "\n").replace('\r', '\n');
String[] lines = normalized.split("\n", -1);
StringBuilder builder = new StringBuilder();
int linesToWrite = Math.min(lines.length, safeMaxLines);
for (int index = 0; index < linesToWrite; index++) {
if (builder.length() > 0) {
builder.append('\n');
}
builder.append(lines[index]);
}
if (builder.length() > safeMaxLength) {
builder.setLength(safeMaxLength);
}
return builder.toString();
}
protected List<RoomUnit> resolveUsers(WiredContext ctx) {
return WiredSourceUtil.resolveUsers(ctx, this.userSource);
}
@@ -212,7 +246,9 @@ public class WiredEffectWhisper extends InteractionWiredEffect {
}
String msg = buildMessage(ctx, (sharedSourceHabbo != null) ? sharedSourceHabbo : habbo);
habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(msg, habbo, habbo, RoomChatMessageBubbles.getBubble(this.bubbleStyle))));
habbo.getClient().sendResponse(new RoomUserWhisperComposer(
new RoomChatMessage(msg, habbo.getRoomUnit(), RoomChatMessageBubbles.getBubble(this.bubbleStyle))
));
if (habbo.getRoomUnit().isIdle()) {
habbo.getRoomUnit().getRoom().unIdle(habbo);
@@ -170,13 +170,15 @@ public class WiredExtraTextInputVariable extends InteractionWiredExtra {
}
public Integer resolveCapturedValue(Room room, String rawValue) {
String normalizedValue = rawValue != null ? rawValue.trim() : "";
if (normalizedValue.isEmpty()) {
return null;
}
String capturedValue = rawValue != null ? rawValue : "";
String normalizedValue = capturedValue.trim();
if (this.getDisplayType(room) == DISPLAY_TEXTUAL) {
return WiredVariableTextConnectorSupport.toValue(room, this.variableItemId, normalizedValue);
return WiredVariableTextConnectorSupport.toValue(room, this.variableItemId, capturedValue);
}
if (normalizedValue.isEmpty()) {
return null;
}
try {
@@ -22,6 +22,7 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
public static final int CODE = 79;
public static final int MAX_MAPPING_LENGTH = 1000;
public static final int MAX_MAPPING_LINES = 30;
private static final String PRESERVED_SPACE = "\u00A0";
private String mappingsText = "";
private LinkedHashMap<Integer, String> mappings = new LinkedHashMap<>();
@@ -123,8 +124,12 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
return "";
}
String mappedValue = this.mappings.get(value);
return mappedValue != null ? mappedValue : String.valueOf(value);
if (this.mappings.containsKey(value)) {
String mappedValue = this.mappings.get(value);
return mappedValue != null ? preserveSpaces(mappedValue) : "";
}
return String.valueOf(value);
}
public Integer resolveValue(String text) {
@@ -132,17 +137,16 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
return null;
}
String normalizedText = text.trim();
if (normalizedText.isEmpty()) {
return null;
}
String normalizedText = normalizePreservedSpaces(text);
for (Map.Entry<Integer, String> entry : this.mappings.entrySet()) {
if (entry == null || entry.getKey() == null || entry.getValue() == null) {
continue;
}
if (entry.getValue().trim().equalsIgnoreCase(normalizedText)) {
String normalizedMappingValue = normalizePreservedSpaces(entry.getValue());
if (normalizedMappingValue.equalsIgnoreCase(normalizedText)) {
return entry.getKey();
}
}
@@ -195,8 +199,8 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
continue;
}
String line = rawLine.trim();
if (line.isEmpty()) {
String line = rawLine;
if (line.trim().isEmpty()) {
continue;
}
@@ -210,7 +214,7 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
}
String keyPart = line.substring(0, separatorIndex).trim();
String valuePart = line.substring(separatorIndex + 1).trim();
String valuePart = line.substring(separatorIndex + 1);
try {
result.put(Integer.parseInt(keyPart), valuePart);
@@ -221,6 +225,14 @@ public class WiredExtraVariableTextConnector extends InteractionWiredExtra {
return result;
}
private static String preserveSpaces(String value) {
return value.replace(" ", PRESERVED_SPACE);
}
private static String normalizePreservedSpaces(String value) {
return value.replace(PRESERVED_SPACE, " ");
}
static class JsonData {
String mappingsText;
@@ -95,6 +95,11 @@ public class WiredEffectFurniAltitude extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(
@@ -100,6 +100,11 @@ public class WiredEffectFurniArea extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay()));
@@ -155,6 +155,11 @@ public class WiredEffectFurniByType extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(
@@ -38,6 +38,7 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
private static final int MAX_PICKED_FURNI = 20;
private static final int MAX_TILE_OFFSETS = 64;
private static final int GRID_RANGE = 4;
private int sourceType = SOURCE_USER_TRIGGER;
private boolean filterExisting = false;
@@ -69,8 +70,20 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
int totalRaw = 0;
int wiredSkipped = 0;
Set<HabboItem> result = new LinkedHashSet<>();
Set<HabboItem> neighborhoodItems = new LinkedHashSet<>();
for (int[] src : sourcePositions) {
LOGGER.info("[FurniNeighborhood] Source: ({},{}), offsets: {}", src[0], src[1], tileOffsets.size());
for (int[] offset : getFullGridOffsets()) {
int tx = src[0] + (offset[0] - this.targetOffsetX);
int ty = src[1] + (offset[1] - this.targetOffsetY);
for (HabboItem item : room.getItemsAt(tx, ty)) {
if (item != null && (includeWiredItems || !(item instanceof InteractionWired))) {
neighborhoodItems.add(item);
}
}
}
for (int[] offset : tileOffsets) {
int tx = src[0] + (offset[0] - this.targetOffsetX);
int ty = src[1] + (offset[1] - this.targetOffsetY);
@@ -91,7 +104,7 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
}
LOGGER.info("[FurniNeighborhood] Raw={}, wiredSkipped={}, kept={}", totalRaw, wiredSkipped, result.size());
result = this.applySelectorModifiers(result, this.getSelectableFloorItems(room, ctx), ctx.targets().items(), filterExisting, invert);
result = this.applyNeighborhoodModifiers(result, neighborhoodItems, ctx.targets().items());
// Always set the selector result even if empty.
// An empty result means no items matched the neighborhood, so downstream
@@ -100,15 +113,51 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
LOGGER.info("[FurniNeighborhood] Set {} items as targets", result.size());
}
private List<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) {
switch (sourceType) {
case SOURCE_USER_TRIGGER: {
if (ctx.tile().isPresent()) {
return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y });
Optional<RoomUnit> actor = ctx.actor();
if (actor.isPresent()) {
return Collections.singletonList(new int[]{ actor.get().getX(), actor.get().getY() });
}
return ctx.actor()
.map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() }))
return ctx.tile()
.map(tile -> Collections.singletonList(new int[]{ tile.x, tile.y }))
.orElse(Collections.emptyList());
}
case SOURCE_USER_SIGNAL: {
@@ -260,6 +309,16 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect {
return true;
}
@Override
public boolean hasRequiredSelectorTargets(WiredContext ctx) {
return ctx != null && ctx.targets().hasItems();
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(
@@ -128,6 +128,11 @@ public class WiredEffectFurniOnFurni extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()));
@@ -86,6 +86,11 @@ public class WiredEffectFurniPicks extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(
@@ -77,6 +77,11 @@ public class WiredEffectFurniSignal extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay()));
@@ -86,6 +86,11 @@ public class WiredEffectUsersArea extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay()));
@@ -92,6 +92,11 @@ public class WiredEffectUsersByAction extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(
@@ -90,6 +90,11 @@ public class WiredEffectUsersByName extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.namesText, this.filterExisting, this.invert, this.getDelay()));
@@ -76,6 +76,11 @@ public class WiredEffectUsersByType extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.entityType, this.filterExisting, this.invert, this.getDelay()));
@@ -90,6 +90,11 @@ public class WiredEffectUsersGroup extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.groupType, this.selectedGroupId, this.filterExisting, this.invert, this.getDelay()));
@@ -73,6 +73,11 @@ public class WiredEffectUsersHandItem extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.handItemId, this.filterExisting, this.invert, this.getDelay()));
@@ -38,6 +38,7 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
private static final int MAX_PICKED_FURNI = 20;
private static final int MAX_TILE_OFFSETS = 64;
private static final int GRID_RANGE = 4;
private int sourceType = SOURCE_USER_TRIGGER;
private boolean filterExisting = false;
@@ -87,11 +88,25 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
LOGGER.debug("[Neighborhood] Target tiles: {}", targetTiles);
Set<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> neighborhoodUsers = new ArrayList<>();
for (RoomUnit unit : room.getRoomUnits()) {
String pos = unit.getX() + "," + unit.getY();
boolean onTile = targetTiles.contains(pos);
if (neighborhoodTiles.contains(pos)) {
neighborhoodUsers.add(unit);
}
LOGGER.debug("[Neighborhood] Unit id={} type={} pos={} onTile={}", unit.getId(), unit.getRoomUnitType(), pos, onTile);
if (onTile) {
@@ -99,7 +114,7 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
}
}
result = new ArrayList<>(this.applySelectorModifiers(result, room.getRoomUnits(), ctx.targets().users(), filterExisting, invert));
result = new ArrayList<>(this.applyNeighborhoodModifiers(result, neighborhoodUsers, ctx.targets().users()));
LOGGER.debug("[Neighborhood] Result: {} users selected", result.size());
@@ -110,15 +125,51 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
ctx.targets().setUsers(result);
}
private List<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) {
switch (sourceType) {
case SOURCE_USER_TRIGGER: {
if (ctx.tile().isPresent()) {
return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y });
Optional<RoomUnit> actor = ctx.actor();
if (actor.isPresent()) {
return Collections.singletonList(new int[]{ actor.get().getX(), actor.get().getY() });
}
return ctx.actor()
.map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() }))
return ctx.tile()
.map(tile -> Collections.singletonList(new int[]{ tile.x, tile.y }))
.orElse(Collections.emptyList());
}
case SOURCE_USER_SIGNAL: {
@@ -262,6 +313,16 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect {
return true;
}
@Override
public boolean hasRequiredSelectorTargets(WiredContext ctx) {
return ctx != null && ctx.targets().hasUsers();
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(
@@ -115,6 +115,11 @@ public class WiredEffectUsersOnFurni extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()));
@@ -71,6 +71,11 @@ public class WiredEffectUsersSignal extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay()));
@@ -76,6 +76,11 @@ public class WiredEffectUsersTeam extends InteractionWiredEffect {
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
return WiredManager.getGson().toJson(new JsonData(this.teamType, this.filterExisting, this.invert, this.getDelay()));
@@ -187,6 +187,11 @@ public abstract class WiredEffectVariableSelectorBase extends InteractionWiredEf
return true;
}
@Override
public boolean usesExistingSelectorTargets() {
return this.filterExisting;
}
@Override
public String getWiredData() {
this.refreshReferenceItems();
@@ -370,8 +370,14 @@ public class PetManager {
} else {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
LOGGER.error("Missing petdata for type {}. Adding this to the database...", type);
try (PreparedStatement statement = connection.prepareStatement("INSERT INTO pet_actions (pet_type) VALUES (?)")) {
try (PreparedStatement statement = connection.prepareStatement("INSERT INTO pet_actions (pet_type, pet_name, offspring_type, happy_actions, tired_actions, random_actions, can_swim) VALUES (?, ?, ?, ?, ?, ?, ?)")) {
statement.setInt(1, type);
statement.setString(2, getFallbackPetName(type));
statement.setInt(3, getFallbackOffspringType(type));
statement.setString(4, "");
statement.setString(5, "");
statement.setString(6, "");
statement.setString(7, "0");
statement.execute();
}
@@ -411,6 +417,42 @@ public class PetManager {
return this.petData.values();
}
private static String getFallbackPetName(int type) {
switch (type) {
case 0:
return "Dog";
case 1:
return "Cat";
case 2:
return "Crocodile";
case 3:
return "Terrier";
case 4:
return "Bear";
case 5:
return "Pig";
default:
return "pet_type_" + type;
}
}
private static int getFallbackOffspringType(int type) {
switch (type) {
case 0:
return 29;
case 1:
return 28;
case 3:
return 25;
case 4:
return 24;
case 5:
return 30;
default:
return -1;
}
}
public Pet createPet(Item item, String name, String race, String color, GameClient client) {
int type = Integer.parseInt(item.getName().toLowerCase().replace("a0 pet", ""));
@@ -540,4 +582,4 @@ public class PetManager {
return false;
}
}
}
@@ -1177,7 +1177,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
if (this.needsUpdate) {
try (Connection connection = Emulator.getDatabase().getDataSource()
.getConnection(); PreparedStatement statement = connection.prepareStatement(
"UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ?, youtube_enabled = ? WHERE id = ?")) {
"UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ?, youtube_enabled = ?, builders_club_trial_locked = ?, builders_club_original_state = ? WHERE id = ?")) {
statement.setString(1, this.name);
statement.setString(2, this.description);
statement.setString(3, this.password);
@@ -1228,7 +1228,9 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
statement.setString(39, this.hideWired ? "1" : "0");
statement.setString(40, this.allowUnderpass ? "1" : "0");
statement.setString(41, this.youtubeEnabled ? "1" : "0");
statement.setInt(42, this.id);
statement.setString(42, this.buildersClubTrialLocked ? "1" : "0");
statement.setString(43, (this.buildersClubOriginalState != null ? this.buildersClubOriginalState : RoomState.OPEN).name().toLowerCase());
statement.setInt(44, this.id);
statement.executeUpdate();
this.needsUpdate = false;
} catch (SQLException e) {
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.core.DatabaseLoggable;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserCustomizationData;
import com.eu.habbo.messages.ISerialize;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.incoming.Incoming;
@@ -204,23 +205,14 @@ public class RoomChatMessage implements Runnable, ISerialize, DatabaseLoggable {
message.appendInt(this.getMessage().length());
// Custom prefix data
String prefixText = "";
String prefixColor = "";
String prefixIcon = "";
String prefixEffect = "";
if (this.habbo != null && this.habbo.getInventory() != null && this.habbo.getInventory().getPrefixesComponent() != null) {
com.eu.habbo.habbohotel.users.UserPrefix activePrefix = this.habbo.getInventory().getPrefixesComponent().getActivePrefix();
if (activePrefix != null) {
prefixText = activePrefix.getText();
prefixColor = activePrefix.getColor();
prefixIcon = activePrefix.getIcon();
prefixEffect = activePrefix.getEffect();
}
}
message.appendString(prefixText);
message.appendString(prefixColor);
message.appendString(prefixIcon);
message.appendString(prefixEffect);
UserCustomizationData customizationData = (this.habbo != null) ? UserCustomizationData.fromHabbo(this.habbo) : UserCustomizationData.empty();
message.appendString(customizationData.prefixText);
message.appendString(customizationData.prefixColor);
message.appendString(customizationData.prefixIcon);
message.appendString(customizationData.prefixEffect);
message.appendString(customizationData.prefixFont);
message.appendString(customizationData.nickIcon);
message.appendString(customizationData.displayOrder);
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
@@ -343,18 +343,16 @@ public class RoomSpecialTypes {
* Adds a wired trigger to the room.
* @param trigger The trigger to add
*/
public static final int MAX_SIGNAL_SENDERS_PER_ROOM = 25;
public static final int MAX_SIGNAL_RECEIVERS_PER_ROOM = 5;
public static final int MAX_SENDERS_PER_RECEIVER = 5;
public static final int MAX_SIGNAL_SENDERS_PER_ROOM = 0;
public static final int MAX_SIGNAL_RECEIVERS_PER_ROOM = 0;
public static final int MAX_SENDERS_PER_RECEIVER = 0;
public boolean isSignalSenderLimitReached() {
Set<InteractionWiredEffect> existing = this.getSignalSenders();
return existing != null && existing.size() >= MAX_SIGNAL_SENDERS_PER_ROOM;
return false;
}
public boolean isSignalReceiverLimitReached() {
Set<InteractionWiredTrigger> existing = this.wiredTriggers.get(WiredTriggerType.RECEIVE_SIGNAL);
return existing != null && existing.size() >= MAX_SIGNAL_RECEIVERS_PER_ROOM;
return false;
}
public int countSendersTargetingReceiver(int receiverItemId, InteractionWiredEffect excludeSender) {
@@ -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 PetsComponent petsComponent;
private PrefixesComponent prefixesComponent;
private NickIconsComponent nickIconsComponent;
private UserVisualSettingsComponent userVisualSettingsComponent;
public HabboInventory(Habbo habbo) {
this.habbo = habbo;
@@ -68,6 +70,18 @@ public class HabboInventory {
LOGGER.error("Caught exception", e);
}
try {
this.nickIconsComponent = new NickIconsComponent(this.habbo);
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
try {
this.userVisualSettingsComponent = new UserVisualSettingsComponent(this.habbo);
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
this.items = MarketPlace.getOwnOffers(this.habbo);
}
@@ -127,6 +141,22 @@ public class HabboInventory {
this.prefixesComponent = prefixesComponent;
}
public NickIconsComponent getNickIconsComponent() {
return this.nickIconsComponent;
}
public void setNickIconsComponent(NickIconsComponent nickIconsComponent) {
this.nickIconsComponent = nickIconsComponent;
}
public UserVisualSettingsComponent getUserVisualSettingsComponent() {
return this.userVisualSettingsComponent;
}
public void setUserVisualSettingsComponent(UserVisualSettingsComponent userVisualSettingsComponent) {
this.userVisualSettingsComponent = userVisualSettingsComponent;
}
public void dispose() {
this.badgesComponent.dispose();
this.botsComponent.dispose();
@@ -135,6 +165,8 @@ public class HabboInventory {
this.petsComponent.dispose();
this.wardrobeComponent.dispose();
this.prefixesComponent.dispose();
this.nickIconsComponent.dispose();
this.userVisualSettingsComponent.dispose();
this.badgesComponent = null;
this.botsComponent = null;
@@ -143,6 +175,8 @@ public class HabboInventory {
this.petsComponent = null;
this.wardrobeComponent = null;
this.prefixesComponent = null;
this.nickIconsComponent = null;
this.userVisualSettingsComponent = null;
}
public void addMarketplaceOffer(MarketPlaceOffer marketPlaceOffer) {
@@ -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 icon;
private String effect;
private String font;
private int catalogPrefixId;
private String displayName;
private int points;
private int pointsType;
private boolean custom;
private boolean active;
private boolean needsInsert;
private boolean needsUpdate;
@@ -29,6 +35,12 @@ public class UserPrefix implements Runnable {
if (this.icon == null) this.icon = "";
this.effect = set.getString("effect");
if (this.effect == null) this.effect = "";
this.font = readString(set, "font", "");
this.catalogPrefixId = readInt(set, "catalog_prefix_id", 0);
this.displayName = readString(set, "display_name", this.text);
this.points = readInt(set, "points", 0);
this.pointsType = readInt(set, "points_type", 0);
this.custom = readBoolean(set, "is_custom", true);
this.active = set.getBoolean("active");
this.needsInsert = false;
this.needsUpdate = false;
@@ -36,12 +48,22 @@ public class UserPrefix implements Runnable {
}
public UserPrefix(int userId, String text, String color, String icon, String effect) {
this(userId, text, color, icon, effect, "", 0, text, 0, 0, true);
}
public UserPrefix(int userId, String text, String color, String icon, String effect, String font, int catalogPrefixId, String displayName, int points, int pointsType, boolean custom) {
this.id = 0;
this.userId = userId;
this.text = text;
this.color = color;
this.icon = icon != null ? icon : "";
this.effect = effect != null ? effect : "";
this.font = font != null ? font : "";
this.catalogPrefixId = catalogPrefixId;
this.displayName = (displayName != null && !displayName.isEmpty()) ? displayName : text;
this.points = points;
this.pointsType = pointsType;
this.custom = custom;
this.active = false;
this.needsInsert = true;
this.needsUpdate = false;
@@ -54,14 +76,20 @@ public class UserPrefix implements Runnable {
if (this.needsInsert) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO user_prefixes (user_id, text, color, icon, effect, active) VALUES (?, ?, ?, ?, ?, ?)",
"INSERT INTO user_prefixes (user_id, text, color, icon, effect, font, active, catalog_prefix_id, display_name, points, points_type, is_custom) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, this.userId);
statement.setString(2, this.text);
statement.setString(3, this.color);
statement.setString(4, this.icon);
statement.setString(5, this.effect);
statement.setBoolean(6, this.active);
statement.setString(6, this.font);
statement.setBoolean(7, this.active);
statement.setInt(8, this.catalogPrefixId);
statement.setString(9, this.displayName);
statement.setInt(10, this.points);
statement.setInt(11, this.pointsType);
statement.setBoolean(12, this.custom);
statement.execute();
try (ResultSet set = statement.getGeneratedKeys()) {
if (set.next()) {
@@ -82,14 +110,20 @@ public class UserPrefix implements Runnable {
} else if (this.needsUpdate) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE user_prefixes SET text = ?, color = ?, icon = ?, effect = ?, active = ? WHERE id = ? AND user_id = ?")) {
"UPDATE user_prefixes SET text = ?, color = ?, icon = ?, effect = ?, font = ?, active = ?, catalog_prefix_id = ?, display_name = ?, points = ?, points_type = ?, is_custom = ? WHERE id = ? AND user_id = ?")) {
statement.setString(1, this.text);
statement.setString(2, this.color);
statement.setString(3, this.icon);
statement.setString(4, this.effect);
statement.setBoolean(5, this.active);
statement.setInt(6, this.id);
statement.setInt(7, this.userId);
statement.setString(5, this.font);
statement.setBoolean(6, this.active);
statement.setInt(7, this.catalogPrefixId);
statement.setString(8, this.displayName);
statement.setInt(9, this.points);
statement.setInt(10, this.pointsType);
statement.setBoolean(11, this.custom);
statement.setInt(12, this.id);
statement.setInt(13, this.userId);
statement.execute();
}
this.needsUpdate = false;
@@ -109,6 +143,13 @@ public class UserPrefix implements Runnable {
public void setIcon(String icon) { this.icon = icon != null ? icon : ""; }
public String getEffect() { return this.effect; }
public void setEffect(String effect) { this.effect = effect != null ? effect : ""; }
public String getFont() { return this.font; }
public void setFont(String font) { this.font = font != null ? font : ""; }
public int getCatalogPrefixId() { return this.catalogPrefixId; }
public String getDisplayName() { return this.displayName; }
public int getPoints() { return this.points; }
public int getPointsType() { return this.pointsType; }
public boolean isCustom() { return this.custom; }
public boolean isActive() { return this.active; }
public void setActive(boolean active) {
@@ -119,4 +160,29 @@ public class UserPrefix implements Runnable {
public void needsUpdate(boolean needsUpdate) { this.needsUpdate = needsUpdate; }
public void needsInsert(boolean needsInsert) { this.needsInsert = needsInsert; }
public void needsDelete(boolean needsDelete) { this.needsDelete = needsDelete; }
private static int readInt(ResultSet set, String columnName, int defaultValue) {
try {
return set.getInt(columnName);
} catch (SQLException e) {
return defaultValue;
}
}
private static String readString(ResultSet set, String columnName, String defaultValue) {
try {
String value = set.getString(columnName);
return value != null ? value : defaultValue;
} catch (SQLException e) {
return defaultValue;
}
}
private static boolean readBoolean(ResultSet set, String columnName, boolean defaultValue) {
try {
return set.getBoolean(columnName);
} catch (SQLException e) {
return defaultValue;
}
}
}
@@ -0,0 +1,136 @@
package com.eu.habbo.habbohotel.users.infostand;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboStats;
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.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
public class InfostandBackgroundManager {
private static final Logger LOGGER = LoggerFactory.getLogger(InfostandBackgroundManager.class);
public enum Category {
BACKGROUND("background"),
STAND("stand"),
OVERLAY("overlay"),
CARD("card");
public final String dbValue;
Category(String dbValue) {
this.dbValue = dbValue;
}
public static Category fromDbValue(String value) {
for (Category category : values()) {
if (category.dbValue.equalsIgnoreCase(value)) return category;
}
return null;
}
}
private final Map<Category, Map<Integer, Entry>> entries = new EnumMap<>(Category.class);
private boolean enforce = false;
public InfostandBackgroundManager() {
for (Category category : Category.values()) {
this.entries.put(category, Collections.emptyMap());
}
this.reload();
}
public void reload() {
Map<Category, Map<Integer, Entry>> next = new EnumMap<>(Category.class);
for (Category category : Category.values()) {
next.put(category, new HashMap<>());
}
int loaded = 0;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT id, category, min_rank, is_hc_only, is_ambassador_only FROM infostand_backgrounds");
ResultSet set = statement.executeQuery()) {
while (set.next()) {
Category category = Category.fromDbValue(set.getString("category"));
if (category == null) continue;
int id = set.getInt("id");
int minRank = set.getInt("min_rank");
boolean isHcOnly = set.getBoolean("is_hc_only");
boolean isAmbassadorOnly = set.getBoolean("is_ambassador_only");
next.get(category).put(id, new Entry(minRank, isHcOnly, isAmbassadorOnly));
loaded++;
}
} catch (SQLException e) {
this.enforce = false;
for (Category category : Category.values()) {
this.entries.put(category, Collections.emptyMap());
}
LOGGER.error("InfostandBackgroundManager -> Failed to load infostand_backgrounds, server-side validation disabled.", e);
return;
}
for (Category category : Category.values()) {
this.entries.put(category, next.get(category));
}
this.enforce = loaded > 0;
if (this.enforce) {
LOGGER.info("InfostandBackgroundManager -> Loaded {} backgrounds, {} stands, {} overlays, {} cards from infostand_backgrounds.",
this.entries.get(Category.BACKGROUND).size(),
this.entries.get(Category.STAND).size(),
this.entries.get(Category.OVERLAY).size(),
this.entries.get(Category.CARD).size());
} else {
LOGGER.info("InfostandBackgroundManager -> infostand_backgrounds is empty, server-side validation disabled (only range clamp will apply).");
}
}
public boolean canUse(Habbo habbo, Category category, int id) {
if (id == 0) return true;
if (!this.enforce) return true;
if (habbo == null) return false;
Map<Integer, Entry> categoryEntries = this.entries.get(category);
if (categoryEntries == null) return false;
Entry entry = categoryEntries.get(id);
if (entry == null) return false;
HabboInfo info = habbo.getHabboInfo();
int rankId = (info != null && info.getRank() != null) ? info.getRank().getId() : 0;
HabboStats stats = habbo.getHabboStats();
boolean hasClub = stats != null && stats.hasActiveClub();
if (entry.isHcOnly && !hasClub) return false;
if (entry.isAmbassadorOnly && !habbo.hasPermission(Permission.ACC_AMBASSADOR)) return false;
if (rankId < entry.minRank) return false;
return true;
}
public static final class Entry {
public final int minRank;
public final boolean isHcOnly;
public final boolean isAmbassadorOnly;
public Entry(int minRank, boolean isHcOnly, boolean isAmbassadorOnly) {
this.minRank = minRank;
this.isHcOnly = isHcOnly;
this.isAmbassadorOnly = isAmbassadorOnly;
}
}
}
@@ -1,7 +1,6 @@
package com.eu.habbo.habbohotel.users.inventory;
import com.eu.habbo.Emulator;
import com.eu.habbo.database.SqlQueries;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInventory;
@@ -10,6 +9,7 @@ import com.eu.habbo.plugin.events.inventory.InventoryItemAddedEvent;
import com.eu.habbo.plugin.events.inventory.InventoryItemRemovedEvent;
import com.eu.habbo.plugin.events.inventory.InventoryItemsAddedEvent;
import gnu.trove.TCollections;
import gnu.trove.iterator.TIntObjectIterator;
import gnu.trove.map.TIntObjectMap;
import gnu.trove.map.hash.THashMap;
import gnu.trove.map.hash.TIntObjectHashMap;
@@ -18,9 +18,11 @@ import gnu.trove.set.hash.THashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
public class ItemsComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(ItemsComponent.class);
@@ -37,23 +39,25 @@ public class ItemsComponent {
public static THashMap<Integer, HabboItem> loadItems(Habbo habbo) {
THashMap<Integer, HabboItem> itemsList = new THashMap<>();
try {
SqlQueries.forEach(
"SELECT * FROM items WHERE room_id = ? AND user_id = ?",
rs -> {
try {
HabboItem item = Emulator.getGameEnvironment().getItemManager().loadHabboItem(rs);
if (item != null) {
itemsList.put(rs.getInt("id"), item);
} else {
LOGGER.error("Failed to load HabboItem: {}", rs.getInt("id"));
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT items.* FROM items LEFT JOIN builders_club_items ON builders_club_items.item_id = items.id WHERE items.room_id = ? AND items.user_id = ? AND builders_club_items.item_id IS NULL")) {
statement.setInt(1, 0);
statement.setInt(2, habbo.getHabboInfo().getId());
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
try {
HabboItem item = Emulator.getGameEnvironment().getItemManager().loadHabboItem(set);
if (item != null) {
itemsList.put(set.getInt("id"), item);
} else {
LOGGER.error("Failed to load HabboItem: {}", set.getInt("id"));
}
},
0, habbo.getHabboInfo().getId());
} catch (SqlQueries.DataAccessException e) {
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
@@ -147,45 +151,70 @@ public class ItemsComponent {
public void dispose() {
synchronized (this.items) {
if (!this.items.isEmpty()) {
List<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);
}
}
TIntObjectIterator<HabboItem> items = this.items.iterator();
try {
if (!deletes.isEmpty()) {
SqlQueries.batchUpdate(
"DELETE FROM items WHERE id = ?",
deletes,
(ps, item) -> ps.setInt(1, item.getId()));
if (items == null) {
LOGGER.error("Items is NULL!");
return;
}
if (!this.items.isEmpty()) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement updateStmt = connection.prepareStatement(
"UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?")) {
try (PreparedStatement deleteStmt = connection.prepareStatement(
"DELETE FROM items WHERE id = ?")) {
int updateCount = 0;
int deleteCount = 0;
for (int i = this.items.size(); i-- > 0; ) {
try {
items.advance();
} catch (NoSuchElementException e) {
break;
}
HabboItem item = items.value();
if (item.needsDelete()) {
deleteStmt.setInt(1, item.getId());
deleteStmt.addBatch();
deleteCount++;
item.needsUpdate(false);
item.needsDelete(false);
} else if (item.needsUpdate()) {
updateStmt.setInt(1, item.getUserId());
updateStmt.setInt(2, item.getRoomId());
updateStmt.setString(3, item.getWallPosition());
updateStmt.setInt(4, item.getX());
updateStmt.setInt(5, item.getY());
updateStmt.setDouble(6, item.getZ());
updateStmt.setInt(7, item.getRotation());
updateStmt.setString(8, item.getExtradata());
updateStmt.setString(9, item.getLimitedStack() + ":" + item.getLimitedSells());
updateStmt.setInt(10, item.getId());
updateStmt.addBatch();
updateCount++;
item.needsUpdate(false);
}
if (updateCount > 0 && updateCount % 100 == 0) {
updateStmt.executeBatch();
}
if (deleteCount > 0 && deleteCount % 100 == 0) {
deleteStmt.executeBatch();
}
}
if (deleteCount % 100 != 0) {
deleteStmt.executeBatch();
}
if (updateCount % 100 != 0) {
updateStmt.executeBatch();
}
}
}
if (!updates.isEmpty()) {
SqlQueries.batchUpdate(
"UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?",
updates,
(ps, item) -> {
ps.setInt(1, item.getUserId());
ps.setInt(2, item.getRoomId());
ps.setString(3, item.getWallPosition());
ps.setInt(4, item.getX());
ps.setInt(5, item.getY());
ps.setDouble(6, item.getZ());
ps.setInt(7, item.getRotation());
ps.setString(8, item.getExtradata());
ps.setString(9, item.getLimitedStack() + ":" + item.getLimitedSells());
ps.setInt(10, item.getId());
});
}
} catch (SqlQueries.DataAccessException e) {
} catch (SQLException e) {
LOGGER.error("Caught SQL exception during batch item save", e);
}
}
@@ -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;
}
public UserPrefix getPrefixByCatalogId(int catalogPrefixId) {
synchronized (this.prefixes) {
for (UserPrefix prefix : this.prefixes) {
if (prefix.getCatalogPrefixId() == catalogPrefixId) return prefix;
}
}
return null;
}
public void addPrefix(UserPrefix prefix) {
synchronized (this.prefixes) {
this.prefixes.add(prefix);
@@ -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;
}
/**
* 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.
* <p>
@@ -112,7 +112,7 @@ public final class WiredContext {
this.state = state;
this.legacySettings = legacySettings;
this.contextVariables = (event.getContextVariableScope() != null)
? event.getContextVariableScope()
? event.getContextVariableScope().copy()
: new WiredContextVariableScope();
this.targets = new WiredTargets();
@@ -26,6 +26,7 @@ import com.eu.habbo.habbohotel.wired.api.WiredStack;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer;
import com.eu.habbo.messages.outgoing.rooms.items.ItemStateComposer;
import com.eu.habbo.plugin.events.furniture.wired.WiredStackExecutedEvent;
import com.eu.habbo.plugin.events.furniture.wired.WiredStackTriggeredEvent;
import gnu.trove.map.hash.THashMap;
@@ -130,6 +131,9 @@ public final class WiredEngine {
/** Cache room+eventType+sourceItemId -> matching stacks for source-triggered timer events */
private final ConcurrentHashMap<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.
*
@@ -151,6 +155,7 @@ public final class WiredEngine {
this.bannedRooms = new ConcurrentHashMap<>();
this.roomDiagnostics = new ConcurrentHashMap<>();
this.sourceStacksByTriggerKey = new ConcurrentHashMap<>();
this.filteredSelectorAnimationTokens = new ConcurrentHashMap<>();
}
/**
@@ -426,6 +431,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors);
}
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions);
List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution);
boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event);
@@ -541,6 +550,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors);
}
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions);
List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution);
boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event);
@@ -627,6 +640,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors);
}
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions);
List<IWiredEffect> executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution);
return !executableEffects.isEmpty();
@@ -660,6 +677,10 @@ public final class WiredEngine {
applySelectionFilterExtras(stack, ctx, executedSelectors);
}
if (!selectorsHaveRequiredTargets(executedSelectors, ctx)) {
return false;
}
boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, false);
if (!conditionsPassedForExecution) {
return false;
@@ -1011,9 +1032,27 @@ public final class WiredEngine {
if (effects.isEmpty()) return Collections.emptyList();
List<InteractionWiredEffect> executedSelectors = new ArrayList<>();
List<IWiredEffect> immediateSelectors = new ArrayList<>();
List<IWiredEffect> deferredSelectors = new ArrayList<>();
for (IWiredEffect effect : effects) {
if (!effect.isSelector()) continue;
if (effect.usesExistingSelectorTargets()) {
deferredSelectors.add(effect);
} else {
immediateSelectors.add(effect);
}
}
executeSelectorList(immediateSelectors, ctx, executedSelectors);
executeSelectorList(deferredSelectors, ctx, executedSelectors);
return executedSelectors;
}
private void executeSelectorList(List<IWiredEffect> selectors, WiredContext ctx, List<InteractionWiredEffect> executedSelectors) {
for (IWiredEffect effect : selectors) {
if (effect.requiresActor() && !ctx.hasActor()) {
continue;
}
@@ -1022,14 +1061,17 @@ public final class WiredEngine {
try {
effect.execute(ctx);
if (effect instanceof InteractionWiredEffect) {
executedSelectors.add((InteractionWiredEffect) effect);
InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect;
executedSelectors.add(wiredEffect);
if (wiredEffect.usesExistingSelectorTargets()) {
setFilteredSelectorState(ctx.room(), wiredEffect, "3");
}
}
} catch (Exception e) {
LOGGER.warn("Error executing selector: {}", e.getMessage());
}
}
return executedSelectors;
}
private void finalizeSelectors(List<InteractionWiredEffect> executedSelectors, WiredContext ctx, long currentTime) {
@@ -1042,7 +1084,56 @@ public final class WiredEngine {
for (InteractionWiredEffect wiredEffect : executedSelectors) {
wiredEffect.setCooldown(currentTime);
wiredEffect.activateBox(room, actor, currentTime);
if (wiredEffect.usesExistingSelectorTargets()) {
animateFilteredSelectorBox(room, wiredEffect);
} else {
wiredEffect.activateBox(room, actor, currentTime);
}
}
}
private void animateFilteredSelectorBox(Room room, InteractionWiredEffect wiredEffect) {
if (room == null || wiredEffect == null || room.isHideWired()) {
return;
}
long animationToken = System.nanoTime();
this.filteredSelectorAnimationTokens.put(wiredEffect.getId(), animationToken);
setFilteredSelectorState(room, wiredEffect, "3", animationToken, false);
scheduleFilteredSelectorState(room, wiredEffect, "4", animationToken, 80L, false);
scheduleFilteredSelectorState(room, wiredEffect, "5", animationToken, 160L, false);
scheduleFilteredSelectorState(room, wiredEffect, "3", animationToken, 240L, true);
}
private void scheduleFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state, long animationToken, long delay, boolean clearToken) {
Emulator.getThreading().run(() -> setFilteredSelectorState(room, wiredEffect, state, animationToken, clearToken), delay);
}
private void setFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state) {
setFilteredSelectorState(room, wiredEffect, state, 0L, false);
}
private void setFilteredSelectorState(Room room, InteractionWiredEffect wiredEffect, String state, long animationToken, boolean clearToken) {
if (room == null || wiredEffect == null || room.isHideWired()) {
return;
}
if (animationToken != 0L) {
Long currentToken = this.filteredSelectorAnimationTokens.get(wiredEffect.getId());
if (currentToken == null || currentToken != animationToken) {
return;
}
}
if (!state.equals(wiredEffect.getExtradata())) {
wiredEffect.setExtradata(state);
room.sendComposer(new ItemStateComposer(wiredEffect).compose());
}
if (clearToken) {
this.filteredSelectorAnimationTokens.remove(wiredEffect.getId(), animationToken);
}
}
@@ -1059,6 +1150,20 @@ public final class WiredEngine {
WiredSelectionFilterSupport.applySelectorFilters(room, stack.triggerItem(), ctx);
}
private boolean selectorsHaveRequiredTargets(List<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.
*/
@@ -151,8 +151,20 @@ public final class WiredSourceUtil {
selectorCtx.setIncludeWiredSelectorItems(originalCtx.includeWiredSelectorItems());
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) {
if (effect == null || effect.usesExistingSelectorTargets() != deferred) {
continue;
}
if (effect.requiresActor() && !selectorCtx.hasActor()) {
continue;
}
@@ -163,10 +175,6 @@ public final class WiredSourceUtil {
} catch (Exception ignored) {
}
}
applySelectionFilterExtras(room, triggerItem, selectorCtx);
return selectorCtx;
}
private static WiredContext cloneSelectorContext(WiredContext originalCtx, boolean includeWiredItems) {
@@ -64,8 +64,17 @@ public final class WiredTextInputCaptureSupport {
return trigger.matches(stack.triggerItem(), event) ? CaptureResult.matched(new LinkedHashMap<>()) : CaptureResult.noMatch();
}
MatchResult matchResult = matchTemplate(trigger, text, capturersByName);
MatchResult matchResult = matchTemplate(trigger, text, capturersByName, room);
if (!matchResult.matches) {
if (WiredManager.isDebugEnabled()) {
WiredManager.debug("[TextCapture] NO_MATCH room={} triggerId={} mode={} key='{}' text='{}' len={}",
room.getId(),
stack.triggerItem().getId(),
trigger.getMatchMode(),
safeForLog(trigger.getKey()),
safeForLog(text),
(text != null ? text.length() : 0));
}
return CaptureResult.noMatch();
}
@@ -78,12 +87,28 @@ public final class WiredTextInputCaptureSupport {
Integer resolvedValue = capturer.resolveCapturedValue(room, capture.getValue());
if (resolvedValue == null) {
if (WiredManager.isDebugEnabled()) {
WiredManager.debug("[TextCapture] RESOLVE_FAIL room={} triggerId={} capturer='{}' raw='{}' rawLen={}",
room.getId(),
stack.triggerItem().getId(),
capture.getKey(),
safeForLog(capture.getValue()),
(capture.getValue() != null ? capture.getValue().length() : 0));
}
return CaptureResult.noMatch();
}
capturedValues.put(capturer.getVariableItemId(), resolvedValue);
}
if (WiredManager.isDebugEnabled()) {
WiredManager.debug("[TextCapture] MATCH_OK room={} triggerId={} captures={} textLen={}",
room.getId(),
stack.triggerItem().getId(),
capturedValues.size(),
(text != null ? text.length() : 0));
}
return CaptureResult.matched(capturedValues);
}
@@ -108,12 +133,13 @@ public final class WiredTextInputCaptureSupport {
return capturers;
}
private static MatchResult matchTemplate(WiredTriggerHabboSaysKeyword trigger, String rawText, Map<String, WiredExtraTextInputVariable> capturersByName) {
String text = rawText != null ? rawText.trim() : "";
private static MatchResult matchTemplate(WiredTriggerHabboSaysKeyword trigger, String rawText, Map<String, WiredExtraTextInputVariable> capturersByName, Room room) {
String text = rawText != null ? rawText : "";
String normalizedText = text.trim();
String template = trigger.getKey() != null ? trigger.getKey().trim() : "";
if (trigger.getMatchMode() == MATCH_ALL_WORDS && template.isEmpty()) {
if (capturersByName.size() != 1 || text.isEmpty()) {
if (capturersByName.size() != 1 || normalizedText.isEmpty()) {
return MatchResult.noMatch();
}
@@ -123,12 +149,24 @@ public final class WiredTextInputCaptureSupport {
return MatchResult.matched(captures);
}
MatchResult adjacentCaptureResult = matchAdjacentCapturers(template, rawText, capturersByName, room, trigger.getMatchMode());
if (adjacentCaptureResult != null) {
if (WiredManager.isDebugEnabled()) {
WiredManager.debug("[TextCapture] ADJACENT mode used key='{}' textLen={} matched={}",
safeForLog(template),
(rawText != null ? rawText.length() : 0),
adjacentCaptureResult.matches);
}
return adjacentCaptureResult;
}
TemplatePattern pattern = buildPattern(template);
if (pattern == null) {
return MatchResult.noMatch();
}
Matcher matcher = pattern.pattern.matcher(text);
String matchText = pattern.placeholderNames.isEmpty() ? normalizedText : text;
Matcher matcher = pattern.pattern.matcher(matchText);
boolean matches = (trigger.getMatchMode() == MATCH_CONTAINS) ? matcher.find() : matcher.matches();
if (!matches) {
return MatchResult.noMatch();
@@ -142,12 +180,136 @@ public final class WiredTextInputCaptureSupport {
}
String capturedValue = matcher.group(index + 1);
captures.put(placeholderName, capturedValue != null ? capturedValue.trim() : "");
captures.put(placeholderName, normalizeCapturedValue(capturedValue));
}
return MatchResult.matched(captures);
}
private static MatchResult matchAdjacentCapturers(String template, String rawText, Map<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) {
if (template == null || template.isEmpty()) {
return null;
@@ -160,7 +322,7 @@ public final class WiredTextInputCaptureSupport {
while (matcher.find()) {
regex.append(Pattern.quote(template.substring(cursor, matcher.start())));
regex.append("(.+?)");
regex.append(hasPlaceholderAfter(template, matcher.end()) ? "(.+?)" : "(.+)");
String placeholderName = matcher.group(1) != null ? matcher.group(1).trim().toLowerCase() : "";
placeholderNames.add(placeholderName);
@@ -176,6 +338,10 @@ public final class WiredTextInputCaptureSupport {
return new TemplatePattern(Pattern.compile(regex.toString(), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE), placeholderNames);
}
private static boolean hasPlaceholderAfter(String template, int cursor) {
return PLACEHOLDER_PATTERN.matcher(template.substring(cursor)).find();
}
public static void applyToContext(WiredContext ctx, Room room, CaptureResult captureResult) {
if (ctx == null || room == null || captureResult == null || !captureResult.matches || captureResult.capturedValues.isEmpty()) {
return;
@@ -32,6 +32,8 @@ import java.util.List;
import java.util.Locale;
public final class WiredTextPlaceholderUtil {
private static final char PRESERVED_SPACE = '\u00A0';
private WiredTextPlaceholderUtil() {
}
@@ -87,7 +89,41 @@ public final class WiredTextPlaceholderUtil {
}
}
return resolvedText;
return preserveRepeatedSpaces(resolvedText);
}
private static String preserveRepeatedSpaces(String text) {
if (text == null || text.length() < 2) {
return text;
}
StringBuilder result = new StringBuilder(text.length());
int index = 0;
while (index < text.length()) {
char currentChar = text.charAt(index);
if (currentChar != ' ') {
result.append(currentChar);
index++;
continue;
}
int startIndex = index;
while (index < text.length() && text.charAt(index) == ' ') {
index++;
}
int spaceCount = index - startIndex;
if (spaceCount == 1) {
result.append(' ');
continue;
}
for (int spaceIndex = 0; spaceIndex < spaceCount; spaceIndex++) {
result.append(PRESERVED_SPACE);
}
}
return result.toString();
}
public static boolean requiresActor(Room room, HabboItem stackItem) {
@@ -275,7 +311,7 @@ public final class WiredTextPlaceholderUtil {
}
String value = resolveRoomVariableValue(room, extra);
return (value == null || value.isEmpty()) ? List.of() : List.of(value);
return value == null ? List.of() : List.of(value);
}
private static List<String> collectContextVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) {
@@ -284,7 +320,7 @@ public final class WiredTextPlaceholderUtil {
}
String value = resolveContextVariableValue(ctx, extra);
return (value == null || value.isEmpty()) ? List.of() : List.of(value);
return value == null ? List.of() : List.of(value);
}
private static String resolveUserVariableValue(Room room, RoomUnit roomUnit, WiredExtraTextOutputVariable extra) {
@@ -11,6 +11,8 @@ import java.util.List;
import java.util.Map;
public final class WiredVariableTextConnectorSupport {
private static final String PRESERVED_SPACE = "\u00A0";
private WiredVariableTextConnectorSupport() {
}
@@ -71,7 +73,7 @@ public final class WiredVariableTextConnectorSupport {
Map<Integer, String> mappings = connector.getMappings();
if (mappings.containsKey(value)) {
String mappedValue = mappings.get(value);
return mappedValue != null ? mappedValue : String.valueOf(value);
return mappedValue != null ? preserveSpaces(mappedValue) : "";
}
}
@@ -83,10 +85,7 @@ public final class WiredVariableTextConnectorSupport {
return null;
}
String normalizedText = text.trim();
if (normalizedText.isEmpty()) {
return null;
}
String normalizedText = normalizePreservedSpaces(text);
for (WiredExtraVariableTextConnector connector : getConnectors(room, definitionItemId)) {
Integer mappedValue = connector.resolveValue(normalizedText);
@@ -97,4 +96,12 @@ public final class WiredVariableTextConnectorSupport {
return null;
}
private static String preserveSpaces(String value) {
return value.replace(" ", PRESERVED_SPACE);
}
private static String normalizePreservedSpaces(String value) {
return value.replace(PRESERVED_SPACE, " ");
}
}
@@ -36,6 +36,7 @@ import com.eu.habbo.messages.incoming.helper.MySanctionStatusEvent;
import com.eu.habbo.messages.incoming.helper.RequestTalentTrackEvent;
import com.eu.habbo.messages.incoming.hotelview.*;
import com.eu.habbo.messages.incoming.inventory.*;
import com.eu.habbo.messages.incoming.inventory.nickicons.*;
import com.eu.habbo.messages.incoming.inventory.prefixes.*;
import com.eu.habbo.messages.incoming.modtool.*;
import com.eu.habbo.messages.incoming.navigator.*;
@@ -61,6 +62,8 @@ import com.eu.habbo.messages.incoming.rooms.promotions.RequestPromotionRoomsEven
import com.eu.habbo.messages.incoming.rooms.promotions.UpdateRoomPromotionEvent;
import com.eu.habbo.messages.incoming.rooms.users.*;
import com.eu.habbo.messages.incoming.trading.*;
import com.eu.habbo.messages.incoming.translation.TranslationLanguagesRequestEvent;
import com.eu.habbo.messages.incoming.translation.TranslationTextRequestEvent;
import com.eu.habbo.messages.incoming.unknown.RequestResolutionEvent;
import com.eu.habbo.messages.incoming.unknown.UnknownEvent1;
import com.eu.habbo.messages.incoming.users.*;
@@ -117,6 +120,7 @@ public class PacketManager {
this.registerGuilds();
this.registerPets();
this.registerWired();
this.registerTranslation();
this.registerAchievements();
this.registerFloorPlanEditor();
this.registerAmbassadors();
@@ -409,6 +413,13 @@ public class PacketManager {
this.registerHandler(Incoming.SetActivePrefixEvent, SetActivePrefixEvent.class);
this.registerHandler(Incoming.DeletePrefixEvent, DeletePrefixEvent.class);
this.registerHandler(Incoming.PurchasePrefixEvent, PurchasePrefixEvent.class);
this.registerHandler(Incoming.PurchaseCatalogPrefixEvent, PurchaseCatalogPrefixEvent.class);
this.registerHandler(Incoming.SetDisplayOrderEvent, SetDisplayOrderEvent.class);
// Nick Icons
this.registerHandler(Incoming.RequestUserNickIconsEvent, RequestUserNickIconsEvent.class);
this.registerHandler(Incoming.PurchaseNickIconEvent, PurchaseNickIconEvent.class);
this.registerHandler(Incoming.SetActiveNickIconEvent, SetActiveNickIconEvent.class);
}
void registerRooms() throws Exception {
@@ -635,6 +646,11 @@ public class PacketManager {
this.registerHandler(Incoming.WiredUserInspectMoveEvent, WiredUserInspectMoveEvent.class);
}
void registerTranslation() throws Exception {
this.registerHandler(Incoming.TranslationLanguagesRequestEvent, TranslationLanguagesRequestEvent.class);
this.registerHandler(Incoming.TranslationTextRequestEvent, TranslationTextRequestEvent.class);
}
void registerUnknown() throws Exception {
this.registerHandler(Incoming.RequestResolutionEvent, RequestResolutionEvent.class);
this.registerHandler(Incoming.RequestTalenTrackEvent, RequestTalentTrackEvent.class);
@@ -419,6 +419,8 @@ public class Incoming {
public static final int WiredUserVariableUpdateEvent = 10025;
public static final int WiredUserVariableManageEvent = 10026;
public static final int WiredUserInspectMoveEvent = 10027;
public static final int TranslationLanguagesRequestEvent = 10032;
public static final int TranslationTextRequestEvent = 10033;
public static final int RequestInventoryPetDelete = 10030;
public static final int RequestInventoryBadgeDelete = 10031;
@@ -448,6 +450,11 @@ public class Incoming {
public static final int SetActivePrefixEvent = 7012;
public static final int DeletePrefixEvent = 7013;
public static final int PurchasePrefixEvent = 7014;
public static final int RequestUserNickIconsEvent = 7015;
public static final int PurchaseNickIconEvent = 7016;
public static final int SetActiveNickIconEvent = 7017;
public static final int PurchaseCatalogPrefixEvent = 7018;
public static final int SetDisplayOrderEvent = 7019;
// YouTube Room Broadcast
public static final int YouTubeRoomPlayEvent = 8001;
@@ -29,7 +29,9 @@ public class GuildAcceptMembershipEvent extends MessageHandler {
if (guild != null) {
GuildMember groupMember = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo());
if (userId == this.client.getHabbo().getHabboInfo().getId() || guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || groupMember.getRank().equals(GuildRank.ADMIN) || groupMember.getRank().equals(GuildRank.OWNER) || this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)) {
if (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId()
|| this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)
|| (groupMember != null && (groupMember.getRank().equals(GuildRank.ADMIN) || groupMember.getRank().equals(GuildRank.OWNER)))) {
if (habbo != null) {
if (habbo.getHabboStats().hasGuild(guild.getId())) {
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.ALREADY_ACCEPTED));
@@ -11,6 +11,11 @@ import com.eu.habbo.messages.outgoing.guilds.GuildMemberUpdateComposer;
import com.eu.habbo.plugin.events.guilds.GuildGivenAdminEvent;
public class GuildSetAdminEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
int guildId = this.packet.readInt();
@@ -48,7 +48,7 @@ public class GuildForumPostThreadEvent extends MessageHandler {
if (threadId == 0) {
if (!((guild.canPostThreads().state == 0)
|| (guild.canPostThreads().state == 1 && member != null)
|| (guild.canPostThreads().state == 1 && member != null && member.getRank().type <= GuildRank.MEMBER.type)
|| (guild.canPostThreads().state == 2 && member != null && (member.getRank().type < GuildRank.MEMBER.type))
|| (guild.canPostThreads().state == 3 && guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId())
|| isStaff)) {
@@ -87,7 +87,7 @@ public class GuildForumPostThreadEvent extends MessageHandler {
}
if (!((guild.canPostMessages().state == 0)
|| (guild.canPostMessages().state == 1 && member != null)
|| (guild.canPostMessages().state == 1 && member != null && member.getRank().type <= GuildRank.MEMBER.type)
|| (guild.canPostMessages().state == 2 && member != null && (member.getRank().type < GuildRank.MEMBER.type))
|| (guild.canPostMessages().state == 3 && guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId())
|| isStaff)) {
@@ -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.outgoing.generic.alerts.BubbleAlertKeys;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer;
import com.eu.habbo.messages.outgoing.inventory.prefixes.PrefixReceivedComposer;
import com.eu.habbo.messages.outgoing.users.UserCreditsComposer;
import com.eu.habbo.messages.outgoing.users.UserCurrencyComposer;
@@ -19,6 +20,7 @@ import java.sql.SQLException;
public class PurchasePrefixEvent extends MessageHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(PurchasePrefixEvent.class);
private static final String[] ALLOWED_FONTS = { "", "pixel", "cherry", "vampiro" };
@Override
public int getRatelimit() {
@@ -31,6 +33,7 @@ public class PurchasePrefixEvent extends MessageHandler {
String color = this.packet.readString();
String icon = this.packet.readString();
String effect = this.packet.readString();
String font = this.packet.readString();
Habbo habbo = this.client.getHabbo();
@@ -42,6 +45,9 @@ public class PurchasePrefixEvent extends MessageHandler {
int priceCredits = getSettingInt("price_credits", 5);
int pricePoints = getSettingInt("price_points", 0);
int pointsType = getSettingInt("points_type", 0);
int fontPriceCredits = getSettingInt("font_price_credits", 10);
int fontPricePoints = getSettingInt("font_price_points", 0);
int fontPointsType = getSettingInt("font_points_type", pointsType);
// Validate text
text = text.trim();
@@ -72,43 +78,67 @@ public class PurchasePrefixEvent extends MessageHandler {
return;
}
if (icon == null) icon = "";
icon = icon.trim();
if (effect == null) effect = "";
effect = effect.trim();
if (font == null) font = "";
font = font.trim().toLowerCase();
if (!isAllowedFont(font)) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Invalid font format."));
return;
}
int totalPriceCredits = priceCredits + (!font.isEmpty() ? fontPriceCredits : 0);
// Check credits
if (priceCredits > 0 && habbo.getHabboInfo().getCredits() < priceCredits) {
if (totalPriceCredits > 0 && habbo.getHabboInfo().getCredits() < totalPriceCredits) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough credits."));
return;
}
int totalPricePointsSameType = pricePoints + ((fontPricePoints > 0 && fontPointsType == pointsType && !font.isEmpty()) ? fontPricePoints : 0);
// Check points
if (pricePoints > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < pricePoints) {
if (totalPricePointsSameType > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < totalPricePointsSameType) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points."));
return;
}
if (!font.isEmpty() && fontPricePoints > 0 && fontPointsType != pointsType && habbo.getHabboInfo().getCurrencyAmount(fontPointsType) < fontPricePoints) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points."));
return;
}
// Deduct currency
if (priceCredits > 0) {
habbo.getHabboInfo().addCredits(-priceCredits);
if (totalPriceCredits > 0) {
habbo.getHabboInfo().addCredits(-totalPriceCredits);
this.client.sendResponse(new UserCreditsComposer(habbo));
}
if (pricePoints > 0) {
habbo.getHabboInfo().addCurrencyAmount(pointsType, -pricePoints);
if (totalPricePointsSameType > 0) {
habbo.getHabboInfo().addCurrencyAmount(pointsType, -totalPricePointsSameType);
this.client.sendResponse(new UserCurrencyComposer(habbo));
}
// Validate icon (allow empty or known icon names)
if (icon == null) icon = "";
icon = icon.trim();
// Validate effect
if (effect == null) effect = "";
effect = effect.trim();
if (!font.isEmpty() && fontPricePoints > 0 && fontPointsType != pointsType) {
habbo.getHabboInfo().addCurrencyAmount(fontPointsType, -fontPricePoints);
this.client.sendResponse(new UserCurrencyComposer(habbo));
}
// Create prefix
UserPrefix prefix = new UserPrefix(habbo.getHabboInfo().getId(), text, color, icon, effect);
int storedPoints = totalPricePointsSameType;
int storedPointsType = (storedPoints > 0) ? pointsType : ((!font.isEmpty() && fontPricePoints > 0) ? fontPointsType : pointsType);
UserPrefix prefix = new UserPrefix(habbo.getHabboInfo().getId(), text, color, icon, effect, font, 0, text, storedPoints, storedPointsType, true);
prefix.run(); // Insert into DB synchronously to get the ID
habbo.getInventory().getPrefixesComponent().addPrefix(prefix);
this.client.sendResponse(new PrefixReceivedComposer(prefix));
this.client.sendResponse(new UserNickIconsComposer(habbo));
}
private int getSettingInt(String key, int defaultValue) {
@@ -142,4 +172,14 @@ public class PurchasePrefixEvent extends MessageHandler {
}
return false;
}
private boolean isAllowedFont(String font) {
for (String allowedFont : ALLOWED_FONTS) {
if (allowedFont.equals(font)) {
return true;
}
}
return false;
}
}
@@ -3,6 +3,8 @@ package com.eu.habbo.messages.incoming.inventory.prefixes;
import com.eu.habbo.habbohotel.users.UserPrefix;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.inventory.prefixes.ActivePrefixUpdatedComposer;
import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer;
public class SetActivePrefixEvent extends MessageHandler {
@Override
@@ -12,6 +14,11 @@ public class SetActivePrefixEvent extends MessageHandler {
if (prefixId == 0) {
this.client.getHabbo().getInventory().getPrefixesComponent().deactivateAll();
this.client.sendResponse(new ActivePrefixUpdatedComposer(null));
this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo()));
if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) {
this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose());
}
return;
}
@@ -21,5 +28,10 @@ public class SetActivePrefixEvent extends MessageHandler {
this.client.getHabbo().getInventory().getPrefixesComponent().setActive(prefixId);
this.client.sendResponse(new ActivePrefixUpdatedComposer(prefix));
this.client.sendResponse(new UserNickIconsComposer(this.client.getHabbo()));
if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) {
this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose());
}
}
}
@@ -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());
});
}
}
@@ -1,26 +1,77 @@
package com.eu.habbo.messages.incoming.users;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboStats;
import com.eu.habbo.habbohotel.users.infostand.InfostandBackgroundManager;
import com.eu.habbo.habbohotel.users.infostand.InfostandBackgroundManager.Category;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer;
public class ChangeInfostandBgEvent extends MessageHandler {
private static final String COOLDOWN_KEY = "infostand_bg_cooldown";
private static final long COOLDOWN_MS = 500L;
private static final int MIN_ID = 0;
private static final int MAX_ID = 9999;
@Override
public void handle() throws Exception {
int backgroundImage = this.packet.readInt();
int backgroundStand = this.packet.readInt();
int backgroundOverlay = this.packet.readInt();
int backgroundCard = this.packet.bytesAvailable() >= 4 ? this.packet.readInt() : 0;
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
this.client.getHabbo().getHabboInfo().setInfostandBg(backgroundImage);
this.client.getHabbo().getHabboInfo().setInfostandStand(backgroundStand);
this.client.getHabbo().getHabboInfo().setInfostandOverlay(backgroundOverlay);
this.client.getHabbo().getHabboInfo().setInfostandCardBg(backgroundCard);
this.client.getHabbo().getHabboInfo().run();
HabboInfo info = habbo.getHabboInfo();
if (info == null) return;
if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) {
this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose());
HabboStats stats = habbo.getHabboStats();
if (stats != null) {
long now = System.currentTimeMillis();
Object last = stats.cache.get(COOLDOWN_KEY);
if (last instanceof Long && (now - (Long) last) < COOLDOWN_MS) {
return;
}
stats.cache.put(COOLDOWN_KEY, now);
}
int requestedBg = sanitize(this.packet.readInt());
int requestedStand = sanitize(this.packet.readInt());
int requestedOverlay = sanitize(this.packet.readInt());
int requestedCard = this.packet.bytesAvailable() >= 4 ? sanitize(this.packet.readInt()) : 0;
InfostandBackgroundManager manager = Emulator.getGameEnvironment() != null ? Emulator.getGameEnvironment().getInfostandBackgroundManager() : null;
int backgroundImage = resolve(manager, habbo, Category.BACKGROUND, requestedBg, info.getInfostandBg());
int backgroundStand = resolve(manager, habbo, Category.STAND, requestedStand, info.getInfostandStand());
int backgroundOverlay = resolve(manager, habbo, Category.OVERLAY, requestedOverlay, info.getInfostandOverlay());
int backgroundCard = resolve(manager, habbo, Category.CARD, requestedCard, info.getInfostandCardBg());
if (info.getInfostandBg() == backgroundImage
&& info.getInfostandStand() == backgroundStand
&& info.getInfostandOverlay() == backgroundOverlay
&& info.getInfostandCardBg() == backgroundCard) {
return;
}
info.setInfostandBg(backgroundImage);
info.setInfostandStand(backgroundStand);
info.setInfostandOverlay(backgroundOverlay);
info.setInfostandCardBg(backgroundCard);
info.run();
if (info.getCurrentRoom() != null) {
info.getCurrentRoom().sendComposer(new RoomUserDataComposer(habbo).compose());
} else {
this.client.sendResponse(new RoomUserDataComposer(this.client.getHabbo()));
this.client.sendResponse(new RoomUserDataComposer(habbo));
}
}
}
private static int sanitize(int value) {
if (value < MIN_ID || value > MAX_ID) return 0;
return value;
}
private static int resolve(InfostandBackgroundManager manager, Habbo habbo, Category category, int requested, int current) {
if (manager == null) return requested;
return manager.canUse(habbo, category, requested) ? requested : current;
}
}
@@ -9,6 +9,7 @@ import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.wired.core.WiredManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.generic.alerts.UpdateFailedComposer;
import com.eu.habbo.messages.outgoing.rooms.items.ItemStateComposer;
import com.eu.habbo.messages.outgoing.wired.WiredSavedComposer;
public class WiredEffectSaveDataEvent extends MessageHandler {
@@ -39,6 +40,16 @@ public class WiredEffectSaveDataEvent extends MessageHandler {
if (saved) {
this.client.sendResponse(new WiredSavedComposer());
if (effect != null) {
if (effect.isSelector()) {
if (effect.usesExistingSelectorTargets()) {
effect.setExtradata("3");
room.sendComposer(new ItemStateComposer(effect).compose());
} else if ("3".equals(effect.getExtradata()) || "4".equals(effect.getExtradata()) || "5".equals(effect.getExtradata())) {
effect.setExtradata("0");
room.sendComposer(new ItemStateComposer(effect).compose());
}
}
effect.needsUpdate(true);
Emulator.getThreading().run(effect);
} else {
@@ -124,6 +124,8 @@ public class Outgoing {
public final static int WiredRoomSettingsDataComposer = 5102; // CUSTOM
public final static int WiredUserVariablesDataComposer = 5103; // CUSTOM
public final static int ConfInvisStateComposer = 5104; // CUSTOM
public final static int TranslationLanguagesComposer = 5106; // CUSTOM
public final static int TranslationResultComposer = 5107; // CUSTOM
public final static int AreaHideComposer = 6001; // CUSTOM
public final static int RoomPaintComposer = 2454; // PRODUCTION-201611291003-338511768
public final static int MarketplaceConfigComposer = 1823; // PRODUCTION-201611291003-338511768
@@ -576,6 +578,7 @@ public class Outgoing {
public static final int UserPrefixesComposer = 7001;
public static final int PrefixReceivedComposer = 7002;
public static final int ActivePrefixUpdatedComposer = 7003;
public static final int UserNickIconsComposer = 7004;
public static final int AvailableCommandsComposer = 4050;
// YouTube Room Broadcast
@@ -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.getIcon());
this.response.appendString(this.prefix.getEffect());
this.response.appendString(this.prefix.getFont());
} else {
this.response.appendInt(0);
this.response.appendString("");
this.response.appendString("");
this.response.appendString("");
this.response.appendString("");
this.response.appendString("");
}
return this.response;
@@ -20,6 +20,7 @@ public class PrefixReceivedComposer extends MessageComposer {
this.response.appendString(this.prefix.getColor());
this.response.appendString(this.prefix.getIcon());
this.response.appendString(this.prefix.getEffect());
this.response.appendString(this.prefix.getFont());
return this.response;
}
}
@@ -30,6 +30,7 @@ public class UserPrefixesComposer extends MessageComposer {
this.response.appendString(prefix.getColor());
this.response.appendString(prefix.getIcon());
this.response.appendString(prefix.getEffect());
this.response.appendString(prefix.getFont());
this.response.appendInt(prefix.isActive() ? 1 : 0);
}
@@ -1,6 +1,7 @@
package com.eu.habbo.messages.outgoing.rooms.users;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserCustomizationData;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
@@ -24,6 +25,14 @@ public class RoomUserDataComposer extends MessageComposer {
this.response.appendInt(this.habbo.getHabboInfo().getInfostandStand());
this.response.appendInt(this.habbo.getHabboInfo().getInfostandOverlay());
this.response.appendInt(this.habbo.getHabboInfo().getInfostandCardBg());
UserCustomizationData customizationData = UserCustomizationData.fromHabbo(this.habbo);
this.response.appendString(customizationData.nickIcon);
this.response.appendString(customizationData.prefixText);
this.response.appendString(customizationData.prefixColor);
this.response.appendString(customizationData.prefixIcon);
this.response.appendString(customizationData.prefixEffect);
this.response.appendString(customizationData.prefixFont);
this.response.appendString(customizationData.displayOrder);
return this.response;
}
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.bots.Bot;
import com.eu.habbo.habbohotel.guilds.Guild;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserCustomizationData;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
@@ -67,6 +68,14 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendString("");
this.response.appendInt(this.habbo.getHabboStats().getAchievementScore());
this.response.appendBoolean(true);
UserCustomizationData customizationData = UserCustomizationData.fromHabbo(this.habbo);
this.response.appendString(customizationData.nickIcon);
this.response.appendString(customizationData.prefixText);
this.response.appendString(customizationData.prefixColor);
this.response.appendString(customizationData.prefixIcon);
this.response.appendString(customizationData.prefixEffect);
this.response.appendString(customizationData.prefixFont);
this.response.appendString(customizationData.displayOrder);
this.response.appendString(this.habbo.getHabboInfo().getRoomEntryMethod());
this.response.appendInt(this.habbo.getHabboInfo().getRoomEntryTeleportId());
} else if (this.habbos != null) {
@@ -101,6 +110,14 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendString("");
this.response.appendInt(habbo.getHabboStats().getAchievementScore());
this.response.appendBoolean(true);
UserCustomizationData customizationData = UserCustomizationData.fromHabbo(habbo);
this.response.appendString(customizationData.nickIcon);
this.response.appendString(customizationData.prefixText);
this.response.appendString(customizationData.prefixColor);
this.response.appendString(customizationData.prefixIcon);
this.response.appendString(customizationData.prefixEffect);
this.response.appendString(customizationData.prefixFont);
this.response.appendString(customizationData.displayOrder);
this.response.appendString(habbo.getHabboInfo().getRoomEntryMethod());
this.response.appendInt(habbo.getHabboInfo().getRoomEntryTeleportId());
}
@@ -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.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.UserCustomizationData;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
@@ -116,6 +117,14 @@ public class UserProfileComposer extends MessageComposer {
this.response.appendInt(this.habboInfo.getInfostandStand());
this.response.appendInt(this.habboInfo.getInfostandOverlay());
this.response.appendInt(this.habboInfo.getInfostandCardBg());
UserCustomizationData customizationData = (this.habbo != null) ? UserCustomizationData.fromHabbo(this.habbo) : UserCustomizationData.fromUserId(this.habboInfo.getId());
this.response.appendString(customizationData.nickIcon);
this.response.appendString(customizationData.prefixText);
this.response.appendString(customizationData.prefixColor);
this.response.appendString(customizationData.prefixIcon);
this.response.appendString(customizationData.prefixEffect);
this.response.appendString(customizationData.prefixFont);
this.response.appendString(customizationData.displayOrder);
return this.response;
}
@@ -92,4 +92,4 @@ public abstract class Server {
public int getPort() {
return this.port;
}
}
}
@@ -3,6 +3,8 @@ package com.eu.habbo.networking.gameserver;
import com.eu.habbo.Emulator;
import com.eu.habbo.messages.PacketManager;
import com.eu.habbo.networking.gameserver.auth.AuthHttpHandler;
import com.eu.habbo.networking.gameserver.auth.NitroSecureApiHandler;
import com.eu.habbo.networking.gameserver.auth.NitroSecureAssetHandler;
import com.eu.habbo.networking.gameserver.badges.BadgeHttpHandler;
import com.eu.habbo.networking.gameserver.codec.WebSocketCodec;
import com.eu.habbo.networking.gameserver.crypto.WsHandshakeHandler;
@@ -16,6 +18,7 @@ import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.logging.LoggingHandler;
@@ -53,9 +56,12 @@ public class WebSocketChannelInitializer extends ChannelInitializer<SocketChanne
ch.pipeline().addLast("httpCodec", new HttpServerCodec());
ch.pipeline().addLast("httpAggregator", new HttpObjectAggregator(MAX_FRAME_SIZE));
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("badgeHttpHandler", new BadgeHttpHandler());
ch.pipeline().addLast("wsProtocolHandler", new WebSocketServerProtocolHandler(this.wsConfig));
ch.pipeline().addLast("wsFrameAggregator", new WebSocketFrameAggregator(MAX_FRAME_SIZE));
ch.pipeline().addLast("wsCodec", new WebSocketCodec());
if (Emulator.getConfig().getBoolean("crypto.ws.enabled", false)) {
@@ -0,0 +1,503 @@
package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import com.google.gson.JsonObject;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.mindrot.jbcrypt.BCrypt;
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 static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.EMAIL_RE;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.USERNAME_RE;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.checkPassword;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.errorPayload;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.readString;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.sendJson;
final class AccountChangeEndpoints {
private static final Logger LOGGER = LoggerFactory.getLogger(AccountChangeEndpoints.class);
private AccountChangeEndpoints() {
}
static void handleChangePassword(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
int userId = verifyBearer(req, ip, ctx);
if (userId <= 0) return;
String currentPassword = readString(body, "currentPassword");
String newPassword = readString(body, "newPassword");
String confirmPassword = readString(body, "confirmPassword");
if (currentPassword.isEmpty() || newPassword.isEmpty() || confirmPassword.isEmpty()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("All fields are required."));
return;
}
if (currentPassword.length() > 256 || newPassword.length() > 256) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("Password too long."));
return;
}
if (!newPassword.equals(confirmPassword)) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("New passwords do not match."));
return;
}
if (newPassword.length() < 8) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("Password must be at least 8 characters."));
return;
}
if (newPassword.equals(currentPassword)) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("New password must be different from the current password."));
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
String storedHash = null;
String username = null;
try (PreparedStatement lookup = conn.prepareStatement(
"SELECT username, password FROM users WHERE id = ? LIMIT 1")) {
lookup.setInt(1, userId);
try (ResultSet rs = lookup.executeQuery()) {
if (rs.next()) {
username = rs.getString("username");
storedHash = rs.getString("password");
}
}
}
if (storedHash == null || storedHash.isEmpty()) {
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Account not found."));
return;
}
if (!checkPassword(currentPassword, storedHash)) {
AuthRateLimiter.recordFailure(ip);
LOGGER.info("[auth/change-password] current password mismatch for user id={} username='{}'", userId, username);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED,
errorPayload("Current password is incorrect."));
return;
}
String hashed = BCrypt.hashpw(newPassword, BCrypt.gensalt(12));
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE users SET password = ? WHERE id = ? LIMIT 1")) {
upd.setString(1, hashed);
upd.setInt(2, userId);
upd.executeUpdate();
}
AuthRateLimiter.recordSuccess(ip);
LOGGER.info("[auth/change-password] password updated for user id={} username='{}' ip='{}'", userId, username, ip);
JsonObject ok = new JsonObject();
ok.addProperty("message", "Password updated successfully.");
sendJson(ctx, req, HttpResponseStatus.OK, ok);
} catch (Exception e) {
LOGGER.error("[auth/change-password] failed for user id=" + userId, e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
static void handleChangeEmail(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
int userId = verifyBearer(req, ip, ctx);
if (userId <= 0) return;
String currentPassword = readString(body, "currentPassword");
String newEmail = readString(body, "newEmail").trim();
if (currentPassword.isEmpty() || newEmail.isEmpty()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("All fields are required."));
return;
}
if (currentPassword.length() > 256 || newEmail.length() > 254) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("Field too long."));
return;
}
if (!EMAIL_RE.matcher(newEmail).matches()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("Invalid email address."));
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
String storedHash = null;
String username = null;
String currentEmail = null;
try (PreparedStatement lookup = conn.prepareStatement(
"SELECT username, password, mail FROM users WHERE id = ? LIMIT 1")) {
lookup.setInt(1, userId);
try (ResultSet rs = lookup.executeQuery()) {
if (rs.next()) {
username = rs.getString("username");
storedHash = rs.getString("password");
currentEmail = rs.getString("mail");
}
}
}
if (storedHash == null || storedHash.isEmpty()) {
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Account not found."));
return;
}
if (!checkPassword(currentPassword, storedHash)) {
AuthRateLimiter.recordFailure(ip);
LOGGER.info("[auth/change-email] password mismatch for user id={} username='{}'", userId, username);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED,
errorPayload("Current password is incorrect."));
return;
}
if (currentEmail != null && currentEmail.equalsIgnoreCase(newEmail)) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("New email must be different from the current email."));
return;
}
try (PreparedStatement check = conn.prepareStatement(
"SELECT id FROM users WHERE mail = ? AND id <> ? LIMIT 1")) {
check.setString(1, newEmail);
check.setInt(2, userId);
try (ResultSet rs = check.executeQuery()) {
if (rs.next()) {
sendJson(ctx, req, HttpResponseStatus.CONFLICT,
errorPayload("That email address is already in use."));
return;
}
}
}
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE users SET mail = ? WHERE id = ? LIMIT 1")) {
upd.setString(1, newEmail);
upd.setInt(2, userId);
upd.executeUpdate();
}
if (currentEmail != null && !currentEmail.isEmpty()) AvailabilityCache.invalidateEmail(currentEmail);
AvailabilityCache.invalidateEmail(newEmail);
AuthRateLimiter.recordSuccess(ip);
LOGGER.info("[auth/change-email] email updated for user id={} username='{}' ip='{}'", userId, username, ip);
JsonObject ok = new JsonObject();
ok.addProperty("message", "Email updated successfully.");
ok.addProperty("email", newEmail);
sendJson(ctx, req, HttpResponseStatus.OK, ok);
} catch (Exception e) {
LOGGER.error("[auth/change-email] failed for user id=" + userId, e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
static void handleChangeUsername(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
int userId = verifyBearer(req, ip, ctx);
if (userId <= 0) return;
String currentPassword = readString(body, "currentPassword");
String newUsername = readString(body, "newUsername").trim();
if (currentPassword.isEmpty() || newUsername.isEmpty()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("All fields are required."));
return;
}
if (currentPassword.length() > 256) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("Field too long."));
return;
}
if (newUsername.length() > 25) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("Username can be at most 25 characters."));
return;
}
if (!USERNAME_RE.matcher(newUsername).matches()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("Username must be 3-25 characters (letters, numbers, . _ -)."));
return;
}
long cooldownDays = Math.max(0, Emulator.getConfig().getInt("rename.cooldown_days", 30));
long cooldownSeconds = cooldownDays * 86400L;
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
String storedHash = null;
String currentUsername = null;
int lastChange = 0;
boolean cooldownColumnExists = true;
try (PreparedStatement lookup = conn.prepareStatement(
"SELECT username, password, last_username_change FROM users WHERE id = ? LIMIT 1")) {
lookup.setInt(1, userId);
try (ResultSet rs = lookup.executeQuery()) {
if (rs.next()) {
currentUsername = rs.getString("username");
storedHash = rs.getString("password");
lastChange = rs.getInt("last_username_change");
}
}
} catch (SQLException missingColumn) {
cooldownColumnExists = false;
LOGGER.warn("[auth/change-username] users.last_username_change column missing — cooldown disabled. Run the migration in config/Database.sql.");
try (PreparedStatement lookup = conn.prepareStatement(
"SELECT username, password FROM users WHERE id = ? LIMIT 1")) {
lookup.setInt(1, userId);
try (ResultSet rs = lookup.executeQuery()) {
if (rs.next()) {
currentUsername = rs.getString("username");
storedHash = rs.getString("password");
}
}
}
}
if (storedHash == null || storedHash.isEmpty()) {
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Account not found."));
return;
}
if (!checkPassword(currentPassword, storedHash)) {
AuthRateLimiter.recordFailure(ip);
LOGGER.info("[auth/change-username] password mismatch for user id={} username='{}'", userId, currentUsername);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED,
errorPayload("Current password is incorrect."));
return;
}
if (currentUsername != null && currentUsername.equals(newUsername)) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("New username must be different from the current username."));
return;
}
int now = Emulator.getIntUnixTimestamp();
if (cooldownColumnExists && cooldownSeconds > 0 && lastChange > 0) {
long allowedAt = (long) lastChange + cooldownSeconds;
if (now < allowedAt) {
long remaining = allowedAt - now;
long days = remaining / 86400L;
long hours = (remaining % 86400L) / 3600L;
String wait = days > 0 ? (days + " day" + (days == 1 ? "" : "s")) : (hours + " hour" + (hours == 1 ? "" : "s"));
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
errorPayload("You can rename again in " + wait + "."));
return;
}
}
try (PreparedStatement banned = conn.prepareStatement(
"SELECT 1 FROM banned_usernames WHERE LOWER(username) = LOWER(?) LIMIT 1")) {
banned.setString(1, newUsername);
try (ResultSet rs = banned.executeQuery()) {
if (rs.next()) {
sendJson(ctx, req, HttpResponseStatus.CONFLICT,
errorPayload("That username is not allowed."));
return;
}
}
} catch (SQLException bannedTableError) {
if (bannedTableError.getErrorCode() != 1146
&& !"42S02".equals(bannedTableError.getSQLState())) {
throw bannedTableError;
}
}
try (PreparedStatement check = conn.prepareStatement(
"SELECT id FROM users WHERE LOWER(username) = LOWER(?) AND id <> ? LIMIT 1")) {
check.setString(1, newUsername);
check.setInt(2, userId);
try (ResultSet rs = check.executeQuery()) {
if (rs.next()) {
sendJson(ctx, req, HttpResponseStatus.CONFLICT,
errorPayload("That username is already taken."));
return;
}
}
}
boolean previousAutoCommit = conn.getAutoCommit();
conn.setAutoCommit(false);
boolean cooldownRace = false;
boolean duplicateName = false;
try {
int rowsUpdated = 0;
try (PreparedStatement upd = conn.prepareStatement(
cooldownColumnExists
? "UPDATE users SET username = ?, last_username_change = ? "
+ "WHERE id = ? "
+ " AND (last_username_change = 0 OR last_username_change + ? <= ?) "
+ "LIMIT 1"
: "UPDATE users SET username = ? WHERE id = ? LIMIT 1")) {
upd.setString(1, newUsername);
if (cooldownColumnExists) {
upd.setInt(2, now);
upd.setInt(3, userId);
upd.setLong(4, cooldownSeconds);
upd.setInt(5, now);
} else {
upd.setInt(2, userId);
}
try {
rowsUpdated = upd.executeUpdate();
} catch (SQLException dup) {
if (dup.getErrorCode() == 1062 || "23000".equals(dup.getSQLState())) {
duplicateName = true;
} else {
throw dup;
}
}
}
if (duplicateName || (cooldownColumnExists && rowsUpdated == 0)) {
if (!duplicateName) cooldownRace = true;
conn.rollback();
} else {
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE rooms SET owner_name = ? WHERE owner_id = ?")) {
upd.setString(1, newUsername);
upd.setInt(2, userId);
upd.executeUpdate();
}
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE rooms_for_sale SET owner_name = ? WHERE user_id = ?")) {
upd.setString(1, newUsername);
upd.setInt(2, userId);
upd.executeUpdate();
} catch (SQLException roomsForSale) {
if (roomsForSale.getErrorCode() != 1146
&& !"42S02".equals(roomsForSale.getSQLState())) {
throw roomsForSale;
}
}
conn.commit();
}
} catch (SQLException txError) {
try { conn.rollback(); } catch (SQLException ignore) {}
throw txError;
} finally {
conn.setAutoCommit(previousAutoCommit);
}
if (duplicateName) {
LOGGER.info("[auth/change-username] dup-entry race for user id={} wanted='{}'", userId, newUsername);
sendJson(ctx, req, HttpResponseStatus.CONFLICT,
errorPayload("That username is already taken."));
return;
}
if (cooldownRace) {
LOGGER.info("[auth/change-username] cooldown race for user id={} (concurrent rename rejected)", userId);
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
errorPayload("Rename already in progress — please wait."));
return;
}
try {
if (Emulator.getGameServer() != null && Emulator.getGameServer().getGameClientManager() != null
&& Emulator.getGameEnvironment() != null && Emulator.getGameEnvironment().getHabboManager() != null) {
com.eu.habbo.habbohotel.users.Habbo habbo =
Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (habbo != null) {
Emulator.getGameEnvironment().getHabboManager().removeHabbo(habbo);
habbo.getHabboInfo().setUsername(newUsername);
Emulator.getGameEnvironment().getHabboManager().addHabbo(habbo);
}
}
} catch (Exception cacheError) {
LOGGER.warn("[auth/change-username] failed to refresh HabboManager cache", cacheError);
}
try {
if (Emulator.getGameEnvironment() != null && Emulator.getGameEnvironment().getRoomManager() != null) {
for (com.eu.habbo.habbohotel.rooms.Room room : Emulator.getGameEnvironment().getRoomManager().getActiveRooms()) {
if (room.getOwnerId() == userId) {
room.setOwnerName(newUsername);
}
}
}
} catch (Exception cacheError) {
LOGGER.warn("[auth/change-username] failed to refresh Room.ownerName cache", cacheError);
}
try {
com.eu.habbo.messages.incoming.catalog.marketplace.RequestOffersEvent.cachedResults.clear();
} catch (Exception cacheError) {
LOGGER.warn("[auth/change-username] failed to clear marketplace cache", cacheError);
}
try (PreparedStatement clear = conn.prepareStatement(
"UPDATE users SET auth_ticket = '', online = '0' WHERE id = ? LIMIT 1")) {
clear.setInt(1, userId);
clear.executeUpdate();
}
if (Emulator.getGameServer() != null
&& Emulator.getGameServer().getGameClientManager() != null) {
com.eu.habbo.habbohotel.users.Habbo habbo =
Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (habbo != null && habbo.getClient() != null) {
Emulator.getGameServer().getGameClientManager().disposeClient(habbo.getClient());
}
}
AuthRateLimiter.recordSuccess(ip);
LOGGER.info("[auth/change-username] '{}' -> '{}' (user id={}, ip='{}')",
currentUsername, newUsername, userId, ip);
JsonObject ok = new JsonObject();
ok.addProperty("message", "Username updated. Please log in again with your new name.");
ok.addProperty("username", newUsername);
ok.addProperty("relogin", true);
sendJson(ctx, req, HttpResponseStatus.OK, ok);
} catch (Exception e) {
LOGGER.error("[auth/change-username] failed for user id=" + userId, e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
private static int verifyBearer(FullHttpRequest req, String ip, ChannelHandlerContext ctx) {
String authHeader = req.headers().get(HttpHeaderNames.AUTHORIZATION);
String bearer = "";
if (authHeader != null && authHeader.regionMatches(true, 0, "Bearer ", 0, 7)) {
bearer = authHeader.substring(7).trim();
}
int userId = AccessTokenService.verify(bearer);
if (userId <= 0) {
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Not authenticated."));
}
return userId;
}
}
@@ -0,0 +1,106 @@
package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import com.google.gson.JsonObject;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.EMAIL_RE;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.USERNAME_RE;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.errorPayload;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.readString;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.sendJson;
final class AccountCheckEndpoints {
private static final Logger LOGGER = LoggerFactory.getLogger(AccountCheckEndpoints.class);
private AccountCheckEndpoints() {
}
static void handleCheckEmail(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
if (!AuthRateLimiter.tryProbe(ip)) {
long secs = AuthRateLimiter.secondsUntilProbeReset(ip);
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
errorPayload("Too many requests. Try again in " + secs + "s."));
return;
}
String email = readString(body, "email").trim();
if (email.isEmpty() || email.length() > 254 || !EMAIL_RE.matcher(email).matches()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Invalid email address."));
return;
}
Boolean cached = AvailabilityCache.lookupEmail(email);
boolean taken;
if (cached != null) {
taken = !cached;
} else {
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT 1 FROM users WHERE mail = ? LIMIT 1")) {
stmt.setString(1, email);
try (ResultSet rs = stmt.executeQuery()) {
taken = rs.next();
}
} catch (Exception e) {
LOGGER.error("check-email failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
return;
}
AvailabilityCache.storeEmail(email, !taken);
}
JsonObject res = new JsonObject();
res.addProperty("available", !taken);
if (taken) res.addProperty("error", "This email is already in use.");
sendJson(ctx, req, HttpResponseStatus.OK, res);
}
static void handleCheckUsername(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
if (!AuthRateLimiter.tryProbe(ip)) {
long secs = AuthRateLimiter.secondsUntilProbeReset(ip);
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
errorPayload("Too many requests. Try again in " + secs + "s."));
return;
}
String username = readString(body, "username").trim();
if (!USERNAME_RE.matcher(username).matches()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("Username must be 3-32 chars (letters, numbers, . _ -)."));
return;
}
Boolean cached = AvailabilityCache.lookupUsername(username);
boolean taken;
if (cached != null) {
taken = !cached;
} else {
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT 1 FROM users WHERE username = ? LIMIT 1")) {
stmt.setString(1, username);
try (ResultSet rs = stmt.executeQuery()) {
taken = rs.next();
}
} catch (Exception e) {
LOGGER.error("check-username failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
return;
}
AvailabilityCache.storeUsername(username, !taken);
}
JsonObject res = new JsonObject();
res.addProperty("available", !taken);
if (taken) res.addProperty("error", "This Habbo name is already taken.");
sendJson(ctx, req, HttpResponseStatus.OK, res);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,237 @@
package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import com.eu.habbo.networking.gameserver.GameServerAttributes;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import org.mindrot.jbcrypt.BCrypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Base64;
import java.util.regex.Pattern;
final class AuthHttpUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthHttpUtil.class);
static final Pattern USERNAME_RE = Pattern.compile("^[A-Za-z0-9._-]{3,32}$");
static final Pattern EMAIL_RE = Pattern.compile("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$");
static final Pattern FIGURE_RE = Pattern.compile("^[A-Za-z0-9.\\-]{1,200}$");
static final SecureRandom RNG = new SecureRandom();
static final int MAX_BODY_BYTES = 8 * 1024;
private static final long PERMANENT_BAN_THRESHOLD_SECONDS = 30L * 365L * 24L * 60L * 60L;
private AuthHttpUtil() {
}
static String readString(JsonObject obj, String key) {
if (obj == null || !obj.has(key) || obj.get(key).isJsonNull()) return "";
try {
return obj.get(key).getAsString();
} catch (Exception e) {
return "";
}
}
static int readInt(JsonObject obj, String key, int defaultValue) {
if (obj == null || !obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
try {
return obj.get(key).getAsInt();
} catch (Exception e) {
return defaultValue;
}
}
static boolean readBoolean(JsonObject obj, String key, boolean defaultValue) {
if (obj == null || !obj.has(key) || obj.get(key).isJsonNull()) return defaultValue;
try {
JsonElement el = obj.get(key);
if (el.getAsJsonPrimitive().isBoolean()) return el.getAsBoolean();
String s = el.getAsString();
return "1".equals(s) || "true".equalsIgnoreCase(s);
} catch (Exception e) {
return defaultValue;
}
}
static JsonObject errorPayload(String message) {
JsonObject obj = new JsonObject();
obj.addProperty("error", message);
return obj;
}
static void sendJson(ChannelHandlerContext ctx, FullHttpRequest req,
HttpResponseStatus status, JsonObject body) {
byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(bytes));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; 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);
}
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);
}
static void applyCors(FullHttpRequest req, FullHttpResponse response) {
String origin = req.headers().get(HttpHeaderNames.ORIGIN);
if (origin != null && !origin.isEmpty() && CorsOriginGate.isAllowed(req)) {
response.headers().set("Access-Control-Allow-Origin", origin);
response.headers().set("Access-Control-Allow-Credentials", "true");
}
response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS");
String requestedHeaders = req.headers().get("Access-Control-Request-Headers");
if (requestedHeaders != null && !requestedHeaders.isEmpty()) {
response.headers().set("Access-Control-Allow-Headers", requestedHeaders);
} else {
response.headers().set("Access-Control-Allow-Headers",
"Authorization, Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api");
}
response.headers().set("Vary", "Origin, Access-Control-Request-Headers, Access-Control-Request-Method");
response.headers().set("Access-Control-Max-Age", "600");
response.headers().set("Access-Control-Expose-Headers", "X-Nitro-Sec, X-Nitro-Key-Fp, X-Nitro-Derive-Fp");
}
static boolean isKeepAlive(FullHttpRequest req) {
String connection = req.headers().get(HttpHeaderNames.CONNECTION);
return connection == null || !"close".equalsIgnoreCase(connection);
}
static String resolveClientIp(ChannelHandlerContext ctx, FullHttpRequest req) {
String ipHeader = Emulator.getConfig() != null
? Emulator.getConfig().getValue("ws.ip.header", "")
: "";
if (!ipHeader.isEmpty() && req.headers().contains(ipHeader)) {
String hv = req.headers().get(ipHeader);
if (hv != null && !hv.isEmpty()) {
int comma = hv.indexOf(',');
return (comma > 0 ? hv.substring(0, comma) : hv).trim();
}
}
if (ctx.channel().attr(GameServerAttributes.WS_IP).get() != null) {
return ctx.channel().attr(GameServerAttributes.WS_IP).get();
}
if (ctx.channel().remoteAddress() instanceof InetSocketAddress addr) {
return addr.getAddress().getHostAddress();
}
return "";
}
static boolean checkPassword(String plain, String stored) {
String compatible = stored.startsWith("$2y$") ? "$2a$" + stored.substring(4) : stored;
try {
return BCrypt.checkpw(plain, compatible);
} catch (IllegalArgumentException e) {
return false;
}
}
static String mintSsoTicket() {
byte[] buf = new byte[32];
RNG.nextBytes(buf);
return "nitro-" + Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
}
static String mintResetToken() {
byte[] buf = new byte[32];
RNG.nextBytes(buf);
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
}
static final class BanInfo {
final String type;
final String reason;
final int expiresAt;
BanInfo(String type, String reason, int expiresAt) {
this.type = type == null ? "account" : type;
this.reason = reason == null ? "" : reason;
this.expiresAt = expiresAt;
}
boolean isPermanent() {
return (long) expiresAt - Emulator.getIntUnixTimestamp() > PERMANENT_BAN_THRESHOLD_SECONDS;
}
}
static BanInfo lookupAccountBan(Connection conn, int userId) throws SQLException {
try (PreparedStatement stmt = conn.prepareStatement(
"SELECT ban_expire, ban_reason, type FROM bans " +
"WHERE user_id = ? AND ban_expire >= ? AND (type = 'account' OR type = 'super') " +
"ORDER BY ban_expire DESC LIMIT 1")) {
stmt.setInt(1, userId);
stmt.setInt(2, Emulator.getIntUnixTimestamp());
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return new BanInfo(rs.getString("type"), rs.getString("ban_reason"), rs.getInt("ban_expire"));
}
}
}
return null;
}
static BanInfo lookupIpBan(Connection conn, String ip) throws SQLException {
try (PreparedStatement stmt = conn.prepareStatement(
"SELECT ban_expire, ban_reason, type FROM bans " +
"WHERE ip = ? AND ban_expire >= ? AND (type = 'ip' OR type = 'super') " +
"ORDER BY ban_expire DESC LIMIT 1")) {
stmt.setString(1, ip);
stmt.setInt(2, Emulator.getIntUnixTimestamp());
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return new BanInfo(rs.getString("type"), rs.getString("ban_reason"), rs.getInt("ban_expire"));
}
}
}
return null;
}
static JsonObject bannedPayload(BanInfo ban) {
boolean permanent = ban.isPermanent();
String message = permanent
? "Your account has been permanently banned."
: "Your account is temporarily banned.";
JsonObject details = new JsonObject();
details.addProperty("type", ban.type);
details.addProperty("reason", ban.reason);
details.addProperty("permanent", permanent);
if (!permanent) details.addProperty("expiresAt", ban.expiresAt);
JsonObject obj = new JsonObject();
obj.addProperty("error", message);
obj.add("ban", details);
return obj;
}
}
@@ -0,0 +1,53 @@
package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import java.net.URI;
public final class CorsOriginGate {
private static final String CONFIG_KEY = "ws.whitelist";
private static final String CONFIG_DEFAULT = "localhost";
private CorsOriginGate() {
}
public static boolean isAllowed(FullHttpRequest req) {
if (req == null) return false;
String origin = req.headers().get(HttpHeaderNames.ORIGIN);
if (origin == null || origin.isEmpty()) return false;
String host;
try {
URI uri = new URI(origin);
host = uri.getHost();
} catch (Exception ignored) {
return false;
}
if (host == null || host.isEmpty()) return false;
if (host.startsWith("www.")) host = host.substring(4);
String configured = Emulator.getConfig().getValue(CONFIG_KEY, CONFIG_DEFAULT);
if (configured == null || configured.isEmpty()) return false;
for (String entry : configured.split(",")) {
String trimmed = entry.trim();
if (trimmed.isEmpty()) continue;
if ("*".equals(trimmed)) {
return true;
}
if (trimmed.startsWith("*")) {
String suffix = trimmed.substring(1);
if (host.endsWith(suffix) || ("." + host).equals(suffix)) {
return true;
}
} else if (host.equals(trimmed)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,300 @@
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() && CorsOriginGate.isAllowed(req)) {
response.headers().set("Access-Control-Allow-Origin", origin);
response.headers().set("Access-Control-Allow-Credentials", "true");
}
response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS");
String requestedHeaders = req.headers().get("Access-Control-Request-Headers");
if (requestedHeaders != null && !requestedHeaders.isEmpty()) {
response.headers().set("Access-Control-Allow-Headers", requestedHeaders);
} else {
response.headers().set("Access-Control-Allow-Headers",
"Authorization, Content-Type, X-Requested-With, X-Nitro-Key, X-Nitro-Api");
}
response.headers().set("Vary", "Origin, Access-Control-Request-Headers, Access-Control-Request-Method");
response.headers().set("Access-Control-Max-Age", "600");
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,364 @@
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() && CorsOriginGate.isAllowed(req)) {
response.headers().set("Access-Control-Allow-Origin", origin);
}
response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS");
String requestedHeaders = req.headers().get("Access-Control-Request-Headers");
if (requestedHeaders != null && !requestedHeaders.isEmpty()) {
response.headers().set("Access-Control-Allow-Headers", requestedHeaders);
} else {
response.headers().set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Nitro-Key");
}
response.headers().set("Vary", "Origin, Access-Control-Request-Headers, Access-Control-Request-Method");
response.headers().set("Access-Control-Max-Age", "600");
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) {}
}
@@ -0,0 +1,175 @@
package com.eu.habbo.networking.gameserver.auth;
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.sql.Statement;
final class RegistrationSupport {
private static final Logger LOGGER = LoggerFactory.getLogger(RegistrationSupport.class);
private RegistrationSupport() {
}
static void materializeCustomLayout(Connection conn, int templateId, int newRoomId) {
String overrideModel = "0";
String heightmap = "";
int doorX = 0, doorY = 0, doorDir = 2;
try (PreparedStatement sel = conn.prepareStatement(
"SELECT override_model, heightmap, door_x, door_y, door_dir " +
"FROM room_templates WHERE template_id = ? LIMIT 1")) {
sel.setInt(1, templateId);
try (ResultSet rs = sel.executeQuery()) {
if (rs.next()) {
overrideModel = rs.getString("override_model");
heightmap = rs.getString("heightmap");
doorX = rs.getInt("door_x");
doorY = rs.getInt("door_y");
doorDir = rs.getInt("door_dir");
}
}
} catch (SQLException e) {
LOGGER.error("[auth/register] reading template layout failed templateId=" + templateId, e);
return;
}
if (!"1".equals(overrideModel) || heightmap == null || heightmap.isEmpty()) {
return;
}
String customName = "custom_" + newRoomId;
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO room_models_custom (id, name, door_x, door_y, door_dir, heightmap) " +
"VALUES (?, ?, ?, ?, ?, ?) " +
"ON DUPLICATE KEY UPDATE name = VALUES(name), door_x = VALUES(door_x), " +
"door_y = VALUES(door_y), door_dir = VALUES(door_dir), heightmap = VALUES(heightmap)")) {
ins.setInt(1, newRoomId);
ins.setString(2, customName);
ins.setInt(3, doorX);
ins.setInt(4, doorY);
ins.setInt(5, doorDir);
ins.setString(6, heightmap);
ins.executeUpdate();
} catch (SQLException e) {
LOGGER.error("[auth/register] room_models_custom insert failed roomId=" + newRoomId, e);
return;
}
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE rooms SET model = ? WHERE id = ? LIMIT 1")) {
upd.setString(1, customName);
upd.setInt(2, newRoomId);
upd.executeUpdate();
} catch (SQLException e) {
LOGGER.error("[auth/register] rooms.model rename failed roomId=" + newRoomId, e);
}
LOGGER.info("[auth/register] materialized custom layout '{}' for roomId={}", customName, newRoomId);
}
static void seedUserCurrencies(Connection conn, int userId, int duckets, int diamonds) {
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO users_currency (user_id, type, amount) VALUES (?, ?, ?) " +
"ON DUPLICATE KEY UPDATE amount = VALUES(amount)")) {
if (duckets > 0) {
ins.setInt(1, userId);
ins.setInt(2, 0);
ins.setInt(3, duckets);
ins.addBatch();
}
if (diamonds > 0) {
ins.setInt(1, userId);
ins.setInt(2, 5);
ins.setInt(3, diamonds);
ins.addBatch();
}
ins.executeBatch();
} catch (SQLException e) {
LOGGER.error("[auth/register] seeding users_currency failed userId=" + userId
+ " duckets=" + duckets + " diamonds=" + diamonds, e);
}
}
static void cloneTemplateForUser(Connection conn, int templateId, int userId, String userName) {
LOGGER.info("[auth/register] cloning template id={} for user id={} name='{}'", templateId, userId, userName);
try (PreparedStatement check = conn.prepareStatement(
"SELECT 1 FROM room_templates WHERE template_id = ? AND enabled = '1' LIMIT 1")) {
check.setInt(1, templateId);
try (ResultSet rs = check.executeQuery()) {
if (!rs.next()) {
LOGGER.warn("[auth/register] unknown/disabled room template id={} for user id={}", templateId, userId);
return;
}
}
} catch (SQLException e) {
LOGGER.error("[auth/register] template lookup failed for templateId=" + templateId, e);
return;
}
int newRoomId = 0;
int roomsInserted = 0;
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO rooms (owner_id, owner_name, name, description, model, password, state, " +
"users_max, category, paper_floor, paper_wall, paper_landscape, thickness_wall, " +
"thickness_floor, moodlight_data, override_model, trade_mode) " +
"(SELECT ?, ?, name, room_description, model, password, state, " +
"users_max, category, paper_floor, paper_wall, paper_landscape, thickness_wall, " +
"thickness_floor, moodlight_data, override_model, trade_mode " +
"FROM room_templates WHERE template_id = ?)",
Statement.RETURN_GENERATED_KEYS)) {
ins.setInt(1, userId);
ins.setString(2, userName);
ins.setInt(3, templateId);
roomsInserted = ins.executeUpdate();
try (ResultSet keys = ins.getGeneratedKeys()) {
if (keys.next()) newRoomId = keys.getInt(1);
}
} catch (SQLException e) {
LOGGER.error("[auth/register] clone rooms failed templateId=" + templateId + " userId=" + userId, e);
return;
}
LOGGER.info("[auth/register] rooms insert: rowsAffected={} newRoomId={}", roomsInserted, newRoomId);
if (newRoomId <= 0) {
LOGGER.warn("[auth/register] clone aborted - no roomId returned (templateId={}, userId={})", templateId, userId);
return;
}
materializeCustomLayout(conn, templateId, newRoomId);
int itemsInserted = 0;
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO items (user_id, room_id, item_id, wall_pos, x, y, z, rot, " +
"extra_data, wired_data, limited_data, guild_id) " +
"(SELECT ?, ?, item_id, wall_pos, x, y, z, rot, extra_data, wired_data, '0:0', 0 " +
"FROM room_templates_items WHERE template_id = ?)")) {
ins.setInt(1, userId);
ins.setInt(2, newRoomId);
ins.setInt(3, templateId);
itemsInserted = ins.executeUpdate();
} catch (SQLException e) {
LOGGER.error("[auth/register] clone items failed templateId=" + templateId
+ " roomId=" + newRoomId + " userId=" + userId, e);
}
LOGGER.info("[auth/register] items insert: rowsAffected={} roomId={}", itemsInserted, newRoomId);
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE users SET home_room = ? WHERE id = ? LIMIT 1")) {
upd.setInt(1, newRoomId);
upd.setInt(2, userId);
int rows = upd.executeUpdate();
LOGGER.info("[auth/register] home_room update: rowsAffected={} userId={} roomId={}", rows, userId, newRoomId);
} catch (SQLException e) {
LOGGER.error("[auth/register] setting home_room failed userId=" + userId + " roomId=" + newRoomId, e);
}
}
}
@@ -44,7 +44,9 @@ public final class RememberJwtService {
}
private static int familyTtlDays() {
return Math.max(1, Emulator.getConfig().getInt("login.remember.duration.days", 30));
int configured = Emulator.getConfig().getInt("login.remember.duration.days", 0);
if (configured <= 0) configured = Emulator.getConfig().getInt("login.remember.days", 30);
return Math.max(1, configured);
}
private static long familyTtlSeconds() {
@@ -0,0 +1,466 @@
package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import com.google.gson.JsonObject;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.mindrot.jbcrypt.BCrypt;
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.sql.Statement;
import java.sql.Timestamp;
import java.time.Instant;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.EMAIL_RE;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.FIGURE_RE;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.USERNAME_RE;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.bannedPayload;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.checkPassword;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.errorPayload;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.lookupAccountBan;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.lookupIpBan;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.mintResetToken;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.mintSsoTicket;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.readBoolean;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.readInt;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.readString;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.sendJson;
final class SessionEndpoints {
private static final Logger LOGGER = LoggerFactory.getLogger(SessionEndpoints.class);
private SessionEndpoints() {
}
static void handleLogout(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body) {
String ssoTicket = readString(body, "ssoTicket");
String rememberToken = readString(body, "rememberToken").trim();
JsonObject ok = new JsonObject();
ok.addProperty("message", "Logged out.");
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
int userId = 0;
if (ssoTicket != null && !ssoTicket.isEmpty()) {
try (PreparedStatement lookup = conn.prepareStatement(
"SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) {
lookup.setString(1, ssoTicket);
try (ResultSet rs = lookup.executeQuery()) {
if (rs.next()) userId = rs.getInt("id");
}
}
if (userId > 0) {
try (PreparedStatement clear = conn.prepareStatement(
"UPDATE users SET auth_ticket = '', online = '0' WHERE id = ? LIMIT 1")) {
clear.setInt(1, userId);
clear.executeUpdate();
}
if (Emulator.getGameServer() != null
&& Emulator.getGameServer().getGameClientManager() != null) {
com.eu.habbo.habbohotel.users.Habbo habbo =
Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (habbo != null && habbo.getClient() != null) {
Emulator.getGameServer().getGameClientManager().disposeClient(habbo.getClient());
}
}
}
}
if (!rememberToken.isEmpty()) {
RememberJwtService.revokeFromToken(conn, rememberToken);
}
} catch (Exception e) {
LOGGER.error("Logout cleanup failed", e);
}
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
static void handleRemember(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
String jwt = readString(body, "rememberToken").trim();
if (jwt.isEmpty()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing rememberToken."));
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
RememberJwtService.RotationResult rot = RememberJwtService.rotate(conn, jwt, ip);
if (rot == null) {
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Remember token invalid or expired."));
return;
}
String ssoTicket = mintSsoTicket();
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE users SET auth_ticket = ?, ip_current = ? WHERE id = ? LIMIT 1")) {
upd.setString(1, ssoTicket);
upd.setString(2, ip == null ? "" : ip);
upd.setInt(3, rot.userId);
upd.executeUpdate();
}
JsonObject ok = new JsonObject();
ok.addProperty("ssoTicket", ssoTicket);
ok.addProperty("username", rot.username);
ok.addProperty("rememberToken", rot.jwt);
ok.addProperty("expiresAt", rot.expiresAt);
ok.addProperty("rememberExpiresAt", rot.expiresAt);
AccessTokenService.Issued access = AccessTokenService.issue(rot.userId);
ok.addProperty("accessToken", access.token);
ok.addProperty("accessTokenExpiresAt", access.expiresAt);
sendJson(ctx, req, HttpResponseStatus.OK, ok);
} catch (Exception e) {
LOGGER.error("Remember login failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
static void handleSsoToken(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
String ssoTicket = readString(body, "ssoTicket").trim();
if (ssoTicket.isEmpty() || ssoTicket.length() > 128) {
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing or invalid ssoTicket."));
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement lookup = conn.prepareStatement(
"SELECT id, username FROM users WHERE auth_ticket = ? LIMIT 1")) {
lookup.setString(1, ssoTicket);
try (ResultSet rs = lookup.executeQuery()) {
if (!rs.next()) {
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("SSO ticket not recognised."));
return;
}
int userId = rs.getInt("id");
String username = rs.getString("username");
AuthRateLimiter.recordSuccess(ip);
AccessTokenService.Issued access = AccessTokenService.issue(userId);
JsonObject ok = new JsonObject();
ok.addProperty("username", username);
ok.addProperty("accessToken", access.token);
ok.addProperty("accessTokenExpiresAt", access.expiresAt);
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
} catch (Exception e) {
LOGGER.error("[auth/sso-token] lookup failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
static void handleRefresh(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
String jwt = readString(body, "rememberToken").trim();
if (jwt.isEmpty()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing rememberToken."));
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
RememberJwtService.RotationResult rot = RememberJwtService.rotate(conn, jwt, ip);
if (rot == null) {
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("Remember token invalid or expired."));
return;
}
JsonObject ok = new JsonObject();
ok.addProperty("rememberToken", rot.jwt);
ok.addProperty("expiresAt", rot.expiresAt);
ok.addProperty("rememberExpiresAt", rot.expiresAt);
AccessTokenService.Issued access = AccessTokenService.issue(rot.userId);
ok.addProperty("accessToken", access.token);
ok.addProperty("accessTokenExpiresAt", access.expiresAt);
sendJson(ctx, req, HttpResponseStatus.OK, ok);
} catch (Exception e) {
LOGGER.error("Refresh failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
static void handleLogin(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
String username = readString(body, "username").trim();
String password = readString(body, "password");
boolean rememberMe = readBoolean(body, "remember", false) || readBoolean(body, "rememberMe", false);
if (username.isEmpty() || password.isEmpty()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing credentials."));
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
if (ip != null && !ip.isEmpty()) {
AuthHttpUtil.BanInfo ipBan = lookupIpBan(conn, ip);
if (ipBan != null) {
LOGGER.info("[auth/login] ip ban hit ip={} type={} expires={}",
ip, ipBan.type, ipBan.expiresAt);
sendJson(ctx, req, HttpResponseStatus.FORBIDDEN, bannedPayload(ipBan));
return;
}
}
try (PreparedStatement stmt = conn.prepareStatement(
"SELECT id, username, password FROM users WHERE username = ? LIMIT 1")) {
stmt.setString(1, username);
try (ResultSet rs = stmt.executeQuery()) {
if (!rs.next()) {
LOGGER.info("[auth/login] user not found username='{}' ip={}", username, ip);
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED,
errorPayload("Invalid Habbo name or password."));
return;
}
int userId = rs.getInt("id");
String stored = rs.getString("password");
String storedPreview = stored == null
? "<null>"
: (stored.isEmpty() ? "<empty>" : stored.substring(0, Math.min(7, stored.length())) + "…(" + stored.length() + " chars)");
if (stored == null || stored.isEmpty() || !checkPassword(password, stored)) {
LOGGER.info("[auth/login] password mismatch for user id={} username='{}' stored='{}'",
userId, username, storedPreview);
AuthRateLimiter.recordFailure(ip);
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED,
errorPayload("Invalid Habbo name or password."));
return;
}
AuthHttpUtil.BanInfo accountBan = lookupAccountBan(conn, userId);
if (accountBan != null) {
LOGGER.info("[auth/login] account ban hit userId={} type={} expires={}",
userId, accountBan.type, accountBan.expiresAt);
AuthRateLimiter.recordSuccess(ip);
sendJson(ctx, req, HttpResponseStatus.FORBIDDEN, bannedPayload(accountBan));
return;
}
String ssoTicket = mintSsoTicket();
try (PreparedStatement upd = conn.prepareStatement(
"UPDATE users SET auth_ticket = ?, ip_current = ? WHERE id = ? LIMIT 1")) {
upd.setString(1, ssoTicket);
upd.setString(2, ip == null ? "" : ip);
upd.setInt(3, userId);
upd.executeUpdate();
}
String rememberToken = null;
if (rememberMe) {
try {
RememberJwtService.RotationResult issued = RememberJwtService.issueForNewFamily(
conn, userId, rs.getString("username"), ip);
rememberToken = issued.jwt;
} catch (SQLException e) {
LOGGER.error("Failed to issue remember-me JWT for userId=" + userId, e);
}
}
AuthRateLimiter.recordSuccess(ip);
JsonObject ok = new JsonObject();
ok.addProperty("ssoTicket", ssoTicket);
ok.addProperty("username", rs.getString("username"));
if (rememberToken != null) ok.addProperty("rememberToken", rememberToken);
AccessTokenService.Issued access = AccessTokenService.issue(userId);
ok.addProperty("accessToken", access.token);
ok.addProperty("accessTokenExpiresAt", access.expiresAt);
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
}
} catch (Exception e) {
LOGGER.error("Login query failed for username=" + username, e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
static void handleRegister(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
if (!Emulator.getConfig().getBoolean("login.register.enabled", true)) {
sendJson(ctx, req, HttpResponseStatus.FORBIDDEN, errorPayload("Registration is closed."));
return;
}
String username = readString(body, "username").trim();
String email = readString(body, "email").trim();
String password = readString(body, "password");
String figure = readString(body, "figure").trim();
String gender = readString(body, "gender").trim().toUpperCase();
int templateId = readInt(body, "templateId", 0);
if (!USERNAME_RE.matcher(username).matches()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("Username must be 3-32 chars (letters, numbers, . _ -)."));
return;
}
if (!EMAIL_RE.matcher(email).matches()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Invalid email address."));
return;
}
if (password.length() < 8) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST,
errorPayload("Password must be at least 8 characters."));
return;
}
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) {
int maxPerIp = Emulator.getConfig().getInt("register.max_per_ip", 5);
if (maxPerIp > 0 && ip != null && !ip.isEmpty()) {
try (PreparedStatement quota = conn.prepareStatement(
"SELECT COUNT(*) FROM users WHERE ip_register = ?")) {
quota.setString(1, ip);
try (ResultSet rs = quota.executeQuery()) {
if (rs.next() && rs.getInt(1) >= maxPerIp) {
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
errorPayload("This IP has reached the maximum of "
+ maxPerIp + " registered accounts."));
return;
}
}
}
}
try (PreparedStatement check = conn.prepareStatement(
"SELECT username, mail FROM users WHERE username = ? OR mail = ? LIMIT 1")) {
check.setString(1, username);
check.setString(2, email);
try (ResultSet rs = check.executeQuery()) {
if (rs.next()) {
String existingUser = rs.getString("username");
String existingMail = rs.getString("mail");
boolean userTaken = existingUser != null && existingUser.equalsIgnoreCase(username);
boolean mailTaken = existingMail != null && existingMail.equalsIgnoreCase(email);
String message;
if (userTaken && mailTaken) message = "That Habbo name and email are already in use.";
else if (userTaken) message = "That Habbo name is already in use.";
else message = "That email address is already in use.";
sendJson(ctx, req, HttpResponseStatus.CONFLICT, errorPayload(message));
return;
}
}
}
String hashed = BCrypt.hashpw(password, BCrypt.gensalt(12));
String defaultLook = Emulator.getConfig().getValue("register.default.look",
"hr-100-7.hd-180-1.ch-210-66.lg-270-82.sh-290-80");
String defaultMotto = Emulator.getConfig().getValue("register.default.motto", "I love Habbo!");
int now = Emulator.getIntUnixTimestamp();
String finalLook = (figure.isEmpty() || !FIGURE_RE.matcher(figure).matches()) ? defaultLook : figure;
String finalGender = (gender.equals("M") || gender.equals("F")) ? gender : "M";
int startingCredits = Math.max(0, Emulator.getConfig().getInt("new_user_credits", 0));
int startingDuckets = Math.max(0, Emulator.getConfig().getInt("new_user_duckets", 0));
int startingDiamonds = Math.max(0, Emulator.getConfig().getInt("new_user_diamonds", 0));
int newUserId = 0;
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO users (username, password, mail, account_created, " +
"ip_register, ip_current, last_online, last_login, motto, look, gender, " +
"credits, `rank`, home_room, machine_id, auth_ticket, online) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, '', '', '0')",
Statement.RETURN_GENERATED_KEYS)) {
ins.setString(1, username);
ins.setString(2, hashed);
ins.setString(3, email);
ins.setInt(4, now);
ins.setString(5, ip == null ? "" : ip);
ins.setString(6, ip == null ? "" : ip);
ins.setInt(7, now);
ins.setInt(8, now);
ins.setString(9, defaultMotto);
ins.setString(10, finalLook);
ins.setString(11, finalGender);
ins.setInt(12, startingCredits);
ins.executeUpdate();
try (ResultSet keys = ins.getGeneratedKeys()) {
if (keys.next()) newUserId = keys.getInt(1);
}
}
if (newUserId > 0 && (startingDuckets > 0 || startingDiamonds > 0)) {
RegistrationSupport.seedUserCurrencies(conn, newUserId, startingDuckets, startingDiamonds);
}
LOGGER.info("[auth/register] user created id={} username='{}' templateId={} credits={} duckets={} diamonds={}",
newUserId, username, templateId, startingCredits, startingDuckets, startingDiamonds);
if (newUserId > 0 && templateId > 0) {
RegistrationSupport.cloneTemplateForUser(conn, templateId, newUserId, username);
} else if (templateId > 0) {
LOGGER.warn("[auth/register] skipping template clone: user insert did not return an id (username='{}')", username);
}
AvailabilityCache.invalidateEmail(email);
AvailabilityCache.invalidateUsername(username);
JsonObject ok = new JsonObject();
ok.addProperty("message", "Welcome aboard, " + username + "! Your account is ready — log in below with the password you just chose.");
sendJson(ctx, req, HttpResponseStatus.OK, ok);
} catch (Exception e) {
LOGGER.error("Register query failed for username=" + username, e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
static void handleForgot(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) {
String email = readString(body, "email").trim();
if (!EMAIL_RE.matcher(email).matches()) {
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Invalid email address."));
return;
}
JsonObject ok = new JsonObject();
ok.addProperty("message", "Email sent! If an account matches that address you'll find a reset link in your inbox shortly (check spam if it doesn't show up within a minute).");
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT id, username FROM users WHERE mail = ? LIMIT 1")) {
stmt.setString(1, email);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
int userId = rs.getInt("id");
String username = rs.getString("username");
String token = mintResetToken();
long expiresAt = Instant.now().getEpochSecond() + 60L * 60L; // 1h
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO password_resets (user_id, token, expires_at, created_ip) " +
"VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE " +
"token = VALUES(token), expires_at = VALUES(expires_at), created_ip = VALUES(created_ip)")) {
ins.setInt(1, userId);
ins.setString(2, token);
ins.setTimestamp(3, Timestamp.from(Instant.ofEpochSecond(expiresAt)));
ins.setString(4, ip == null ? "" : ip);
ins.executeUpdate();
}
String resetUrlBase = Emulator.getConfig().getValue("password.reset.url",
"http://localhost/reset-password");
String fullUrl = resetUrlBase + (resetUrlBase.contains("?") ? "&" : "?") + "token=" + token;
String subject = "Reset your Habbo password";
String message = "Hi " + username + ",\n\n" +
"Someone (hopefully you) requested a password reset for your Habbo account.\n" +
"Click the link below within the next hour to choose a new password:\n\n" +
fullUrl + "\n\n" +
"If you didn't request this you can safely ignore this email.";
Emulator.getThreading().getService().submit((Runnable) () -> SmtpMailService.send(email, subject, message));
}
}
} catch (Exception e) {
LOGGER.error("Forgot-password query failed for email=" + email, e);
}
sendJson(ctx, req, HttpResponseStatus.OK, ok);
}
}
@@ -0,0 +1,148 @@
package com.eu.habbo.networking.gameserver.auth;
import com.eu.habbo.Emulator;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.applyCors;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.errorPayload;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.isKeepAlive;
import static com.eu.habbo.networking.gameserver.auth.AuthHttpUtil.sendJson;
final class StaticContentEndpoints {
private static final Logger LOGGER = LoggerFactory.getLogger(StaticContentEndpoints.class);
private static final long NEWS_CACHE_TTL_MS = 30_000L;
private static final int NEWS_IMAGE_MAX_BYTES = 512 * 1024;
private static volatile NewsCacheEntry NEWS_CACHE = null;
private static final class NewsCacheEntry {
final byte[] jsonBytes;
final long expiresAt;
NewsCacheEntry(byte[] j, long e) {
jsonBytes = j;
expiresAt = e;
}
}
private StaticContentEndpoints() {
}
static void handleRoomTemplates(ChannelHandlerContext ctx, FullHttpRequest req) {
JsonArray templates = new JsonArray();
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT template_id, title, description, thumbnail " +
"FROM room_templates WHERE enabled = '1' " +
"ORDER BY sort_order ASC, template_id ASC")) {
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
JsonObject t = new JsonObject();
t.addProperty("templateId", rs.getInt("template_id"));
t.addProperty("title", rs.getString("title"));
t.addProperty("description", rs.getString("description"));
t.addProperty("thumbnail", rs.getString("thumbnail"));
templates.add(t);
}
}
} catch (Exception e) {
LOGGER.error("room-templates list failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
return;
}
JsonObject res = new JsonObject();
res.add("templates", templates);
sendJson(ctx, req, HttpResponseStatus.OK, res);
}
static void handleNews(ChannelHandlerContext ctx, FullHttpRequest req) {
long now = System.currentTimeMillis();
NewsCacheEntry cached = NEWS_CACHE;
if (cached == null || cached.expiresAt < now) {
JsonArray items = new JsonArray();
int limit = Math.max(1, Math.min(20, Emulator.getConfig().getInt("login.news.limit", 5)));
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT id, title, body, image, link_text, link_url " +
"FROM ui_news WHERE enabled = 1 " +
"ORDER BY sort_order ASC, id DESC LIMIT ?")) {
stmt.setInt(1, limit);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
int id = rs.getInt("id");
JsonObject n = new JsonObject();
n.addProperty("id", id);
n.addProperty("title", rs.getString("title"));
n.addProperty("body", rs.getString("body"));
String image = rs.getString("image");
if (image != null && image.length() > NEWS_IMAGE_MAX_BYTES) {
LOGGER.warn("ui_news id={} image is {} bytes (>{}KB cap), omitting in response",
id, image.length(), NEWS_IMAGE_MAX_BYTES / 1024);
image = null;
}
n.addProperty("image", image);
n.addProperty("linkText", rs.getString("link_text"));
n.addProperty("linkUrl", rs.getString("link_url"));
items.add(n);
}
}
} catch (Exception e) {
LOGGER.error("ui_news list failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
return;
}
JsonObject res = new JsonObject();
res.add("news", items);
byte[] bytes = res.toString().getBytes(StandardCharsets.UTF_8);
cached = new NewsCacheEntry(bytes, now + NEWS_CACHE_TTL_MS);
NEWS_CACHE = cached;
}
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
Unpooled.wrappedBuffer(cached.jsonBytes));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8");
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, cached.jsonBytes.length);
response.headers().set(HttpHeaderNames.CACHE_CONTROL, "public, max-age=30");
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);
}
static void handleServerKey(ChannelHandlerContext ctx, FullHttpRequest req) {
try {
JsonObject ok = new JsonObject();
ok.addProperty("publicKey", com.eu.habbo.networking.gameserver.crypto.CryptoSigningKeyManager.publicKeyBase64());
ok.addProperty("algorithm", "ECDSA-P256-SHA256");
sendJson(ctx, req, HttpResponseStatus.OK, ok);
} catch (Exception e) {
LOGGER.error("server-key fetch failed", e);
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error."));
}
}
}

Some files were not shown because too many files have changed in this diff Show More