You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8709a72b6e | |||
| c331da9fbe | |||
| f9a079da02 | |||
| 89eb989c26 | |||
| 47be392d8e | |||
| d9465a0a65 | |||
| 90314d00fe | |||
| 56c73b9d98 | |||
| e6093f959f | |||
| c854770561 | |||
| a0b59134ee | |||
| 67924289ac | |||
| 26326bcc0e | |||
| ee0613a480 | |||
| 37d7885663 | |||
| fdf0e5d806 | |||
| c64d3b7b8d | |||
| c2b85c0c8c | |||
| f8a651b059 | |||
| 59ce829fe0 | |||
| 9bad1eb3f6 | |||
| f51617d092 | |||
| 585af846c4 | |||
| dde2c4143c | |||
| 26999c254b | |||
| dd96523496 | |||
| 02f3ded44e | |||
| 8bbe8640b0 | |||
| 078fb3db60 |
@@ -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';
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+40
-30
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
|
||||
+158
-9
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
|
||||
+66
-20
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
+33
-34
@@ -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 {
|
||||
|
||||
+2
-11
@@ -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");
|
||||
|
||||
+38
-2
@@ -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);
|
||||
|
||||
+7
-5
@@ -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
-10
@@ -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;
|
||||
|
||||
|
||||
+5
@@ -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(
|
||||
|
||||
+5
@@ -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()));
|
||||
|
||||
+5
@@ -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(
|
||||
|
||||
+64
-5
@@ -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(
|
||||
|
||||
+5
@@ -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()));
|
||||
|
||||
+5
@@ -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(
|
||||
|
||||
+5
@@ -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()));
|
||||
|
||||
+5
@@ -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()));
|
||||
|
||||
+5
@@ -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(
|
||||
|
||||
+5
@@ -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()));
|
||||
|
||||
+5
@@ -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()));
|
||||
|
||||
+5
@@ -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()));
|
||||
|
||||
+5
@@ -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()));
|
||||
|
||||
+66
-5
@@ -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(
|
||||
|
||||
+5
@@ -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()));
|
||||
|
||||
+5
@@ -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()));
|
||||
|
||||
+5
@@ -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()));
|
||||
|
||||
+5
@@ -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) {
|
||||
|
||||
+469
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+136
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+85
-56
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+119
@@ -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);
|
||||
|
||||
+94
@@ -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) {
|
||||
|
||||
+173
-7
@@ -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;
|
||||
|
||||
+39
-3
@@ -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) {
|
||||
|
||||
+12
-5
@@ -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;
|
||||
|
||||
+3
-1
@@ -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();
|
||||
|
||||
+2
-2
@@ -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)) {
|
||||
|
||||
+95
@@ -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 : "";
|
||||
}
|
||||
}
|
||||
+11
@@ -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()));
|
||||
}
|
||||
}
|
||||
+34
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
-14
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -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;
|
||||
}
|
||||
}
|
||||
+25
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
+64
-13
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -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
|
||||
|
||||
+217
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -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;
|
||||
|
||||
+1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+9
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+17
@@ -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());
|
||||
}
|
||||
|
||||
+33
@@ -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;
|
||||
}
|
||||
}
|
||||
+29
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -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)) {
|
||||
|
||||
+503
@@ -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;
|
||||
}
|
||||
}
|
||||
+106
@@ -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);
|
||||
}
|
||||
}
|
||||
+68
-1028
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;
|
||||
}
|
||||
}
|
||||
+300
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+364
@@ -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) {}
|
||||
}
|
||||
+175
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
+148
@@ -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
Reference in New Issue
Block a user