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
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fdf0e5d806 | |||
| c64d3b7b8d | |||
| c2b85c0c8c | |||
| f8a651b059 | |||
| 00f9feab14 | |||
| 0b37705b65 | |||
| 9b77ca1016 | |||
| 39941cd496 | |||
| 7095dfad43 | |||
| 750b172304 | |||
| 5afa1f274c | |||
| 8f59eb652f | |||
| 8a8cd1121e | |||
| 60e5ba3a6a | |||
| 9fa3fad70c | |||
| 860f61f765 | |||
| c5137bf3dc | |||
| 5150418796 | |||
| 5c71b318fb | |||
| 1cac407c45 | |||
| d85eecd624 | |||
| c50098a945 | |||
| 0224f3f416 | |||
| 03d37650a0 | |||
| f4e5449443 | |||
| 1ebc8314a8 | |||
| 85a60cf591 | |||
| 41d7420251 | |||
| 5dd602ebab | |||
| 6d203c1267 | |||
| a8bcb27d27 | |||
| b18d65bd79 | |||
| 13958cb11e | |||
| 7414bc2589 | |||
| da2307f3b5 | |||
| 030b5ec174 | |||
| ec54dc5c85 | |||
| 50acf6217e | |||
| dd06f2b15c | |||
| d5497e49ad | |||
| 1916c6c785 | |||
| 4c46c0cb00 | |||
| fdcc33212f | |||
| bcee750ff8 | |||
| 872dd11bd2 | |||
| da0523a794 | |||
| 1184ccdbae | |||
| 1b08e083bf | |||
| 7347906786 | |||
| 3f47321583 | |||
| f6faf36709 | |||
| e2c823253f | |||
| 671ab04b50 |
@@ -1,37 +1,3 @@
|
||||
-- =============================================================================
|
||||
-- Consolidated Database Updates - All-in-One
|
||||
-- =============================================================================
|
||||
-- This file combines ALL individual update scripts from SQL/Database Updates/
|
||||
-- into a single idempotent migration. Every statement is safe to re-run:
|
||||
-- - ALTER TABLE ADD COLUMN IF NOT EXISTS (MariaDB 10.0+)
|
||||
-- - ALTER TABLE CHANGE/MODIFY COLUMN IF EXISTS
|
||||
-- - CREATE TABLE IF NOT EXISTS
|
||||
-- - INSERT IGNORE / ON DUPLICATE KEY UPDATE for settings
|
||||
-- - TRUNCATE + re-insert for reference data (breeding)
|
||||
--
|
||||
-- Run order: This file FIRST, then 001_optimize_gameserver.sql
|
||||
--
|
||||
-- Source files (in applied order):
|
||||
-- 1. UpdateDatabase_Allow_diagonale.sql
|
||||
-- 2. UpdateDatabase_BOT.sql
|
||||
-- 3. UpdateDatabase_Banners.sql
|
||||
-- 4. UpdateDatabase_DanceCMD.sql
|
||||
-- 5. UpdateDatabase_Happiness.sql
|
||||
-- 6. UpdateDatabase_Websocket.sql
|
||||
-- 7. UpdateDatabase_unignorable.sql
|
||||
-- 8. Default_Camera.sql
|
||||
-- 9. 07012026_UpdateDatabase_to_4-0-1.sql
|
||||
-- 10. 09012026_UpdateDatabase_to_4-0-2.sql
|
||||
-- 11. 12012026_Battle Banzai.sql (same as #10, deduplicated)
|
||||
-- 12. 12012026_Breeding Fixes.sql
|
||||
-- 13. 12012026_ChatBubbles.sql
|
||||
-- 14. 16032026_updateall_command.sql
|
||||
-- 15. 17032026_allow_underpass.sql
|
||||
-- 16. 19032026_hotel_timezone.sql
|
||||
-- 17. 21022026_user_prefixes.sql
|
||||
-- 18. 06042026_builders_club_catalog_offers.sql
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
SET @OLD_SQL_MODE = @@SQL_MODE;
|
||||
@@ -512,8 +478,13 @@ ALTER TABLE `users_settings`
|
||||
ADD COLUMN IF NOT EXISTS `builders_club_bonus_furni` INT(11) NOT NULL DEFAULT 0 AFTER `hc_gifts_claimed`;
|
||||
|
||||
|
||||
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`)
|
||||
VALUES ( 'acc_staff_chat', 1, 'Grants access to the in-game Staff Chat group buddy: receives broadcasts from other staff and can broadcast to anyone holding this permission.' )
|
||||
ON DUPLICATE KEY UPDATE `max_value` = VALUES(`max_value`), `comment` = VALUES(`comment`);
|
||||
|
||||
-- =============================================================================
|
||||
-- Done
|
||||
-- Done.
|
||||
-- =============================================================================
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
SET SQL_MODE = @OLD_SQL_MODE;
|
||||
|
||||
@@ -80,7 +80,6 @@ END//
|
||||
DELIMITER ;
|
||||
|
||||
CALL `_add_fk_if_missing`('rooms', 'fk_rooms_owner', 'owner_id', 'users', 'id', 'CASCADE');
|
||||
CALL `_add_fk_if_missing`('items', 'fk_items_user', 'user_id', 'users', 'id', 'CASCADE');
|
||||
CALL `_add_fk_if_missing`('catalog_items', 'fk_catitems_page', 'page_id', 'catalog_pages', 'id', 'CASCADE');
|
||||
CALL `_add_fk_if_missing`('guilds', 'fk_guilds_user', 'user_id', 'users', 'id', 'CASCADE');
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
INSERT INTO emulator_settings (`key`, `value`) VALUES ('wired.tick.workers', '6');
|
||||
@@ -0,0 +1,2 @@
|
||||
INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||
('wired.tick.workers', '6', '');
|
||||
@@ -0,0 +1,135 @@
|
||||
ALTER TABLE emulator_settings
|
||||
CHANGE COLUMN `comment` `comment` TEXT NULL DEFAULT '' ;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `password_resets` (
|
||||
`user_id` INT NOT NULL PRIMARY KEY,
|
||||
`token` VARCHAR(128) NOT NULL,
|
||||
`expires_at` TIMESTAMP NOT NULL,
|
||||
`created_ip` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
UNIQUE KEY `idx_token` (`token`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
|
||||
('login.turnstile.enabled', '0'),
|
||||
('login.turnstile.sitekey', ''),
|
||||
('login.turnstile.secretkey', ''),
|
||||
|
||||
('login.ratelimit.enabled', '1'),
|
||||
('login.ratelimit.max_attempts','5'),
|
||||
('login.ratelimit.window_sec', '60'),
|
||||
('login.ratelimit.lockout_sec', '120'),
|
||||
|
||||
('login.register.enabled', '1'),
|
||||
('register.max_per_ip', '5'),
|
||||
('register.default.look', 'hr-100-7.hd-180-1.ch-210-66.lg-270-82.sh-290-80'),
|
||||
('register.default.motto', 'I love Habbo!'),
|
||||
|
||||
('password.reset.url', 'http://localhost/reset-password'),
|
||||
|
||||
('smtp.provider', 'own'),
|
||||
('smtp.host', 'localhost'),
|
||||
('smtp.port', '587'),
|
||||
('smtp.username', ''),
|
||||
('smtp.password', ''),
|
||||
('smtp.from_address', 'no-reply@example.com'),
|
||||
('smtp.from_name', 'Habbo Hotel'),
|
||||
('smtp.use_tls', '1'),
|
||||
('smtp.use_ssl', '0')
|
||||
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);
|
||||
|
||||
INSERT INTO emulator_settings (`key`, `value`, `comment`) VALUES
|
||||
('new_user_credits', '0' , 'This is the default setting for habbo credits when creating an account for the NitroV3 Login'),
|
||||
('new_user_duckets', '0' , 'This is the default setting for habbo duckets when creating an account for the NitroV3 Login'),
|
||||
('new_user_diamonds', '0' , 'This is the default setting for habbo diamonds when creating an account for the NitroV3 Login')
|
||||
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);
|
||||
|
||||
-- Grant to rank 7 only (adjust rank_7 if your rank id differs)
|
||||
INSERT INTO `permission_definitions` (`permission_key`, `rank_7`, `comment`) VALUES
|
||||
('cmd_setroom_template', '1', 'Use the setroom_template to copy the room into the template')
|
||||
ON DUPLICATE KEY UPDATE `rank_7` = VALUES(`rank_7`);
|
||||
|
||||
INSERT INTO `emulator_texts` (`key`, `value`) VALUES
|
||||
('commands.keys.cmd_setroom_template', 'setroom_template;set_room_template'),
|
||||
('commands.succes.cmd_setroom_template.verify', 'Copy the current room "%roomname%" to room_templates? Type :setroom_template %generic.yes% to confirm.'),
|
||||
('commands.succes.cmd_setroom_template', 'Room saved as template id %id% with %items% items (%skipped% skipped - item_id not in items_base).'),
|
||||
('commands.error.cmd_setroom_template', 'Could not save room as template. Check the server log for details.'),
|
||||
('commands.error.cmd_setroom_template.no_room', 'You must be inside a room to use this command.')
|
||||
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_templates` (
|
||||
`template_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`title` varchar(128) NOT NULL DEFAULT '',
|
||||
`description` varchar(256) NOT NULL DEFAULT '',
|
||||
`thumbnail` varchar(512) NOT NULL DEFAULT '',
|
||||
`sort_order` int(11) NOT NULL DEFAULT 0,
|
||||
`enabled` enum('0','1') NOT NULL DEFAULT '1',
|
||||
`name` varchar(50) NOT NULL DEFAULT '',
|
||||
`room_description` varchar(250) NOT NULL DEFAULT '',
|
||||
`model` varchar(100) NOT NULL,
|
||||
`password` varchar(50) NOT NULL DEFAULT '',
|
||||
`state` enum('open','locked','password','invisible') NOT NULL DEFAULT 'open',
|
||||
`users_max` int(11) NOT NULL DEFAULT 25,
|
||||
`category` int(11) NOT NULL DEFAULT 0,
|
||||
`paper_floor` varchar(50) NOT NULL DEFAULT '0.0',
|
||||
`paper_wall` varchar(50) NOT NULL DEFAULT '0.0',
|
||||
`paper_landscape` varchar(50) NOT NULL DEFAULT '0.0',
|
||||
`thickness_wall` int(11) NOT NULL DEFAULT 0,
|
||||
`thickness_floor` int(11) NOT NULL DEFAULT 0,
|
||||
`moodlight_data` varchar(2048) NOT NULL DEFAULT '',
|
||||
`override_model` enum('0','1') NOT NULL DEFAULT '0',
|
||||
`trade_mode` int(2) NOT NULL DEFAULT 2,
|
||||
`heightmap` mediumtext NOT NULL DEFAULT '',
|
||||
`door_x` int(11) NOT NULL DEFAULT 0,
|
||||
`door_y` int(11) NOT NULL DEFAULT 0,
|
||||
`door_dir` int(4) NOT NULL DEFAULT 2,
|
||||
PRIMARY KEY (`template_id`),
|
||||
KEY `enabled_sort` (`enabled`, `sort_order`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
-- Items belonging to a template. Clone target is `items`.
|
||||
-- `template_id` replaces `room_id`; `user_id` is absent because items
|
||||
-- are re-owned by the new user at clone time.
|
||||
-- --------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `room_templates_items` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`template_id` int(11) NOT NULL,
|
||||
`item_id` int(11) unsigned NOT NULL,
|
||||
`wall_pos` varchar(20) NOT NULL DEFAULT '',
|
||||
`x` int(11) NOT NULL DEFAULT 0,
|
||||
`y` int(11) NOT NULL DEFAULT 0,
|
||||
`z` double(10,6) NOT NULL DEFAULT 0.000000,
|
||||
`rot` int(11) NOT NULL DEFAULT 0,
|
||||
`extra_data` varchar(2096) NOT NULL DEFAULT '',
|
||||
`wired_data` varchar(4096) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `template_id` (`template_id`),
|
||||
CONSTRAINT `fk_rt_items_template`
|
||||
FOREIGN KEY (`template_id`) REFERENCES `room_templates` (`template_id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_rt_items_item_base`
|
||||
FOREIGN KEY (`item_id`) REFERENCES `items_base` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users_remember_families` (
|
||||
`family_id` char(36) NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`current_version` int(11) NOT NULL DEFAULT 1,
|
||||
`created_at` int(11) NOT NULL,
|
||||
`expires_at` int(11) NOT NULL,
|
||||
`revoked` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`last_ip` varchar(45) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`family_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `expires_at` (`expires_at`),
|
||||
CONSTRAINT `fk_remember_family_user`
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
DROP TABLE IF EXISTS `users_remember_tokens`;
|
||||
|
||||
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
|
||||
('login.remember.duration.days', '30'),
|
||||
('login.remember.rotate.interval.minutes', '15'),
|
||||
('login.remember.jwt.secret', '')
|
||||
ON DUPLICATE KEY UPDATE `value` = `value`;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
INSERT INTO `emulator_settings` (`key`, `value`) VALUES
|
||||
('crypto.ws.enabled', '0'),
|
||||
('crypto.ws.signing.enabled', '0'),
|
||||
('crypto.ws.signing.public_key', ''),
|
||||
('crypto.ws.signing.private_key', '')
|
||||
ON DUPLICATE KEY UPDATE `value` = `value`;
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,28 @@
|
||||
-- Make sure that the emulator has write access to the badge_path folder !!!!!
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users_custom_badge_settings` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`badge_path` varchar(255) NOT NULL DEFAULT '/var/www/gamedata/c_images/album1584',
|
||||
`badge_url` varchar(255) NOT NULL DEFAULT '/gamedata/c_images/album1584',
|
||||
`price_badge` int(11) NOT NULL DEFAULT 0,
|
||||
`currency_type` int(11) NOT NULL DEFAULT -1,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
|
||||
INSERT INTO `users_custom_badge_settings` (`id`, `badge_path`, `badge_url`, `price_badge`, `currency_type`)
|
||||
SELECT 1, '/var/www/gamedata/c_images/album1584', '/gamedata/c_images/album1584', 50, 5
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `users_custom_badge_settings` WHERE `id` = 1);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `user_custom_badge` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`badge_id` varchar(64) NOT NULL,
|
||||
`badge_name` varchar(64) NOT NULL DEFAULT '',
|
||||
`badge_description` varchar(255) NOT NULL DEFAULT '',
|
||||
`date_created` int(11) NOT NULL DEFAULT 0,
|
||||
`date_edit` int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `badge_id` (`badge_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
CONSTRAINT `fk_user_custom_badge_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `background_card_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_overlay_id`;
|
||||
+8
-41
@@ -1,8 +1,3 @@
|
||||
-- ============================================================
|
||||
-- Custom Prefix System - Complete Setup
|
||||
-- ============================================================
|
||||
|
||||
-- 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,
|
||||
@@ -46,34 +41,6 @@ INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES
|
||||
('mod'),
|
||||
('owner');
|
||||
|
||||
-- 4. Add effect column (if table already exists without it)
|
||||
-- ALTER TABLE `user_prefixes` ADD COLUMN IF NOT EXISTS `effect` VARCHAR(50) NOT NULL DEFAULT '' AFTER `icon`;
|
||||
|
||||
-- ============================================================
|
||||
-- Catalog page for custom prefixes
|
||||
-- ============================================================
|
||||
-- NOTE: Adjust parent_id to match your catalog parent category ID.
|
||||
-- Example: parent_id = -1 for root, or the ID of your "Extra" / "Specials" category
|
||||
|
||||
INSERT INTO `catalog_pages` (
|
||||
`parent_id`, `caption`, `caption_save`, `icon_image`, `visible`, `enabled`,
|
||||
`min_rank`, `page_layout`, `page_strings_1`, `page_strings_2`
|
||||
) VALUES (
|
||||
-1,
|
||||
'Custom Prefix',
|
||||
'custom_prefix',
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
'custom_prefix',
|
||||
'Create your own custom prefix!\rChoose text, colors, icon and effects to stand out in chat.',
|
||||
''
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Command texts (insert into emulator_texts if not present)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
|
||||
-- GivePrefix command
|
||||
('commands.keys.cmd_give_prefix', 'giveprefix'),
|
||||
@@ -105,11 +72,11 @@ INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES
|
||||
('commands.succes.cmd_prefix_blacklist.added', 'Word "%word%" added to prefix blacklist.'),
|
||||
('commands.succes.cmd_prefix_blacklist.removed', 'Word "%word%" removed from prefix blacklist.');
|
||||
|
||||
-- ============================================================
|
||||
-- Permissions for prefix commands (add to permissions table)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO `permissions` (`id`, `rank_id`, `permission_name`, `setting_type`) VALUES
|
||||
(NULL, 7, 'cmd_give_prefix', '1'),
|
||||
(NULL, 7, 'cmd_list_prefixes', '1'),
|
||||
(NULL, 7, 'cmd_remove_prefix', '1'),
|
||||
(NULL, 7, 'cmd_prefix_blacklist', '1');
|
||||
INSERT IGNORE INTO permission_definitions
|
||||
(permission_key, max_value, rank_1, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7)
|
||||
VALUES
|
||||
('cmd_give_prefix', '1', '0', '0', '0', '0', '0', '0', '1'),
|
||||
('cmd_list_prefixes', '1', '0', '0', '0', '0', '0', '0', '1'),
|
||||
('cmd_remove_prefix', '1', '0', '0', '0', '0', '0', '0', '1'),
|
||||
('cmd_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);
|
||||
+18
-2
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.eu.habbo</groupId>
|
||||
<artifactId>Habbo</artifactId>
|
||||
<version>4.1.3</version>
|
||||
<version>4.1.13</version>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
@@ -162,5 +162,21 @@
|
||||
<artifactId>joda-time</artifactId>
|
||||
<version>2.13.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 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>
|
||||
<artifactId>jbcrypt</artifactId>
|
||||
<version>0.4</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Jakarta Mail — used by the built-in forgot-password endpoint
|
||||
when smtp.* keys are configured in emulator_settings -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.angus</groupId>
|
||||
<artifactId>jakarta.mail</artifactId>
|
||||
<version>2.0.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
-- ============================================================
|
||||
-- Catalog & Furni Admin Permission
|
||||
-- Adds acc_catalogfurni permission to the permissions table
|
||||
-- Required by: CatalogAdmin packet handlers (10050-10059)
|
||||
-- ============================================================
|
||||
|
||||
-- 1. Add the column to the permissions table
|
||||
ALTER TABLE `permissions`
|
||||
ADD COLUMN `acc_catalogfurni` ENUM('0','1') NOT NULL DEFAULT '0'
|
||||
AFTER `acc_catalog_ids`;
|
||||
|
||||
-- 2. Enable for Administrator (rank 7) by default
|
||||
UPDATE `permissions` SET `acc_catalogfurni` = '1' WHERE `id` = 7;
|
||||
|
||||
-- Optional: enable for other ranks as needed
|
||||
-- UPDATE `permissions` SET `acc_catalogfurni` = '1' WHERE `id` = 6; -- Super Mod
|
||||
-- UPDATE `permissions` SET `acc_catalogfurni` = '1' WHERE `id` = 5; -- Moderator
|
||||
@@ -28,6 +28,7 @@ public class RoomUserPetComposer extends MessageComposer {
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendString(this.petType + " " + this.race + " " + this.color + " 2 2 -1 0 3 -1 0");
|
||||
this.response.appendInt(this.habbo.getRoomUnit().getId());
|
||||
this.response.appendInt(this.habbo.getRoomUnit().getX());
|
||||
|
||||
@@ -22,6 +22,8 @@ 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.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;
|
||||
@@ -58,6 +60,8 @@ public class GameEnvironment {
|
||||
private SubscriptionManager subscriptionManager;
|
||||
private CalendarManager calendarManager;
|
||||
private RoomChatBubbleManager roomChatBubbleManager;
|
||||
private CustomBadgeManager customBadgeManager;
|
||||
private InfostandBackgroundManager infostandBackgroundManager;
|
||||
|
||||
public void load() throws Exception {
|
||||
LOGGER.info("GameEnvironment -> Loading...");
|
||||
@@ -84,6 +88,8 @@ public class GameEnvironment {
|
||||
this.pollManager = new PollManager();
|
||||
this.calendarManager = new CalendarManager();
|
||||
this.roomChatBubbleManager = new RoomChatBubbleManager();
|
||||
this.customBadgeManager = new CustomBadgeManager();
|
||||
this.infostandBackgroundManager = new InfostandBackgroundManager();
|
||||
|
||||
this.roomManager.loadPublicRooms();
|
||||
this.navigatorManager.loadNavigator();
|
||||
@@ -219,4 +225,12 @@ public class GameEnvironment {
|
||||
public RoomChatBubbleManager getRoomChatBubbleManager() {
|
||||
return roomChatBubbleManager;
|
||||
}
|
||||
|
||||
public CustomBadgeManager getCustomBadgeManager() {
|
||||
return this.customBadgeManager;
|
||||
}
|
||||
|
||||
public InfostandBackgroundManager getInfostandBackgroundManager() {
|
||||
return this.infostandBackgroundManager;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ public class CommandHandler {
|
||||
addCommand(new ControlCommand());
|
||||
addCommand(new CoordsCommand());
|
||||
addCommand(new CreditsCommand());
|
||||
addCommand(new DanceCommand());
|
||||
addCommand(new DanceCommand());
|
||||
addCommand(new DiagonalCommand());
|
||||
addCommand(new DisconnectCommand());
|
||||
addCommand(new EjectAllCommand());
|
||||
@@ -230,7 +230,7 @@ public class CommandHandler {
|
||||
addCommand(new MutePetsCommand());
|
||||
addCommand(new PetInfoCommand());
|
||||
addCommand(new PickallCommand());
|
||||
addCommand(new PingCommand());
|
||||
addCommand(new PingCommand());
|
||||
addCommand(new PixelCommand());
|
||||
addCommand(new PluginsCommand());
|
||||
addCommand(new PointsCommand());
|
||||
@@ -253,6 +253,7 @@ public class CommandHandler {
|
||||
addCommand(new SayCommand());
|
||||
addCommand(new SetMaxCommand());
|
||||
addCommand(new SetPollCommand());
|
||||
addCommand(new SetRoomTemplateCommand());
|
||||
addCommand(new SetSpeedCommand());
|
||||
addCommand(new ShoutAllCommand());
|
||||
addCommand(new ShoutCommand());
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.eu.habbo.habbohotel.commands;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.*;
|
||||
|
||||
public class SetRoomTemplateCommand extends Command {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SetRoomTemplateCommand.class);
|
||||
|
||||
public SetRoomTemplateCommand() {
|
||||
super("cmd_setroom_template", Emulator.getTexts().getValue("commands.keys.cmd_setroom_template").split(";"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(GameClient gameClient, String[] params) throws Exception {
|
||||
Room room = gameClient.getHabbo().getHabboInfo().getCurrentRoom();
|
||||
if (room == null) {
|
||||
gameClient.getHabbo().whisper(
|
||||
Emulator.getTexts().getValue("commands.error.cmd_setroom_template.no_room"),
|
||||
RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
String yes = Emulator.getTexts().getValue("generic.yes");
|
||||
|
||||
if (params.length < 2 || !params[1].equalsIgnoreCase(yes)) {
|
||||
gameClient.getHabbo().alert(
|
||||
Emulator.getTexts().getValue("commands.succes.cmd_setroom_template.verify")
|
||||
.replace("%generic.yes%", yes)
|
||||
.replace("%roomname%", room.getName()));
|
||||
return true;
|
||||
}
|
||||
|
||||
int newTemplateId = 0;
|
||||
int itemsCopied = 0;
|
||||
int itemsSkipped = 0;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
try (PreparedStatement insTemplate = connection.prepareStatement(
|
||||
"INSERT INTO room_templates (title, description, thumbnail, sort_order, enabled, " +
|
||||
"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) " +
|
||||
"(SELECT name, description, '', 0, '1', " +
|
||||
"name, description, model, password, state, users_max, category, " +
|
||||
"paper_floor, paper_wall, paper_landscape, thickness_wall, thickness_floor, " +
|
||||
"moodlight_data, override_model, trade_mode " +
|
||||
"FROM rooms WHERE id = ?)",
|
||||
Statement.RETURN_GENERATED_KEYS)) {
|
||||
insTemplate.setInt(1, room.getId());
|
||||
insTemplate.executeUpdate();
|
||||
try (ResultSet keys = insTemplate.getGeneratedKeys()) {
|
||||
if (keys.next()) newTemplateId = keys.getInt(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (newTemplateId <= 0) {
|
||||
gameClient.getHabbo().whisper(
|
||||
Emulator.getTexts().getValue("commands.error.cmd_setroom_template"),
|
||||
RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (room.hasCustomLayout()) {
|
||||
try (PreparedStatement updLayout = connection.prepareStatement(
|
||||
"UPDATE room_templates t " +
|
||||
"JOIN room_models_custom c ON c.id = ? " +
|
||||
"SET t.heightmap = c.heightmap, t.door_x = c.door_x, " +
|
||||
" t.door_y = c.door_y, t.door_dir = c.door_dir " +
|
||||
"WHERE t.template_id = ?")) {
|
||||
updLayout.setInt(1, room.getId());
|
||||
updLayout.setInt(2, newTemplateId);
|
||||
updLayout.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
try (PreparedStatement insItems = connection.prepareStatement(
|
||||
"INSERT INTO room_templates_items (template_id, item_id, wall_pos, x, y, z, rot, extra_data, wired_data) " +
|
||||
"SELECT ?, i.item_id, i.wall_pos, i.x, i.y, i.z, i.rot, i.extra_data, i.wired_data " +
|
||||
"FROM items i JOIN items_base ib ON ib.id = i.item_id " +
|
||||
"WHERE i.room_id = ?")) {
|
||||
insItems.setInt(1, newTemplateId);
|
||||
insItems.setInt(2, room.getId());
|
||||
itemsCopied = insItems.executeUpdate();
|
||||
}
|
||||
|
||||
try (PreparedStatement countTotal = connection.prepareStatement(
|
||||
"SELECT COUNT(*) FROM items WHERE room_id = ?")) {
|
||||
countTotal.setInt(1, room.getId());
|
||||
try (ResultSet rs = countTotal.executeQuery()) {
|
||||
if (rs.next()) itemsSkipped = Math.max(0, rs.getInt(1) - itemsCopied);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("cmd_setroom_template failed for roomId=" + room.getId(), e);
|
||||
gameClient.getHabbo().whisper(
|
||||
Emulator.getTexts().getValue("commands.error.cmd_setroom_template"),
|
||||
RoomChatMessageBubbles.ALERT);
|
||||
return true;
|
||||
}
|
||||
|
||||
gameClient.getHabbo().whisper(
|
||||
Emulator.getTexts().getValue("commands.succes.cmd_setroom_template")
|
||||
.replace("%id%", Integer.toString(newTemplateId))
|
||||
.replace("%items%", Integer.toString(itemsCopied))
|
||||
.replace("%skipped%", Integer.toString(itemsSkipped)),
|
||||
RoomChatMessageBubbles.ALERT);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+59
-37
@@ -1,23 +1,16 @@
|
||||
package com.eu.habbo.habbohotel.gameclients;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomUnit;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserEffectComposer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
/**
|
||||
* Manages a grace period for disconnected users. Instead of immediately
|
||||
* disposing a Habbo when their WebSocket drops, the Habbo is held in
|
||||
* a "ghost" state for a configurable number of seconds. If the same
|
||||
* user reconnects (via SSO ticket) within the grace window, their
|
||||
* existing Habbo object is resumed on the new connection — keeping
|
||||
* them in their room, preserving inventory state, etc.
|
||||
*
|
||||
* Config key: session.reconnect.grace.seconds (default: 30)
|
||||
*/
|
||||
public class SessionResumeManager {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SessionResumeManager.class);
|
||||
@@ -37,12 +30,10 @@ public class SessionResumeManager {
|
||||
return Emulator.getConfig().getInt("session.reconnect.grace.seconds", 30);
|
||||
}
|
||||
|
||||
/**
|
||||
* Park a disconnected Habbo in ghost mode. Their room presence is
|
||||
* preserved, but the old GameClient channel is closed.
|
||||
*
|
||||
* @return true if the habbo was parked (grace period > 0), false if immediate dispose should happen
|
||||
*/
|
||||
public int getPausedEffectId() {
|
||||
return Emulator.getConfig().getInt("session.reconnect.effect.id", 170);
|
||||
}
|
||||
|
||||
public boolean parkHabbo(Habbo habbo, String ssoTicket) {
|
||||
int graceSeconds = getGracePeriodSeconds();
|
||||
if (graceSeconds <= 0) {
|
||||
@@ -51,7 +42,6 @@ public class SessionResumeManager {
|
||||
|
||||
int userId = habbo.getHabboInfo().getId();
|
||||
|
||||
// Cancel any existing ghost session for this user
|
||||
GhostSession existing = ghostSessions.remove(userId);
|
||||
if (existing != null && existing.disposeFuture != null) {
|
||||
existing.disposeFuture.cancel(false);
|
||||
@@ -60,12 +50,18 @@ public class SessionResumeManager {
|
||||
LOGGER.info("[SessionResume] Parking {} (id={}) for {}s grace period",
|
||||
habbo.getHabboInfo().getUsername(), userId, graceSeconds);
|
||||
|
||||
// Restore the SSO ticket so the client can reconnect with the same ticket
|
||||
if (ssoTicket != null && !ssoTicket.isEmpty()) {
|
||||
restoreSsoTicket(userId, ssoTicket);
|
||||
}
|
||||
|
||||
// Schedule the final disconnect after the grace period
|
||||
int previousEffectId = 0;
|
||||
int previousEffectEnd = 0;
|
||||
RoomUnit unit = habbo.getRoomUnit();
|
||||
if (unit != null) {
|
||||
previousEffectId = unit.getEffectId();
|
||||
previousEffectEnd = unit.getEffectEndTimestamp();
|
||||
}
|
||||
|
||||
ScheduledFuture<?> future = Emulator.getThreading().run(() -> {
|
||||
GhostSession ghost = ghostSessions.remove(userId);
|
||||
if (ghost != null) {
|
||||
@@ -75,22 +71,19 @@ public class SessionResumeManager {
|
||||
}
|
||||
}, graceSeconds * 1000);
|
||||
|
||||
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future));
|
||||
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd));
|
||||
|
||||
applyPausedEffect(habbo);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resume a ghost session for the given user ID.
|
||||
*
|
||||
* @return the parked Habbo if found within grace period, null otherwise
|
||||
*/
|
||||
public Habbo resumeSession(int userId) {
|
||||
GhostSession ghost = ghostSessions.remove(userId);
|
||||
if (ghost == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cancel the scheduled dispose
|
||||
if (ghost.disposeFuture != null) {
|
||||
ghost.disposeFuture.cancel(false);
|
||||
}
|
||||
@@ -98,19 +91,15 @@ public class SessionResumeManager {
|
||||
LOGGER.info("[SessionResume] Resuming session for {} (id={})",
|
||||
ghost.habbo.getHabboInfo().getUsername(), userId);
|
||||
|
||||
restorePausedEffect(ghost);
|
||||
|
||||
return ghost.habbo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has a ghost session (is in grace period).
|
||||
*/
|
||||
public boolean hasGhostSession(int userId) {
|
||||
return ghostSessions.containsKey(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately expire all ghost sessions (e.g. on emulator shutdown).
|
||||
*/
|
||||
public void disposeAll() {
|
||||
for (GhostSession ghost : ghostSessions.values()) {
|
||||
if (ghost.disposeFuture != null) {
|
||||
@@ -121,9 +110,6 @@ public class SessionResumeManager {
|
||||
ghostSessions.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the actual full disconnect that normally happens in Habbo.disconnect().
|
||||
*/
|
||||
private void performFullDisconnect(Habbo habbo) {
|
||||
try {
|
||||
habbo.getHabboInfo().setOnline(false);
|
||||
@@ -132,7 +118,6 @@ public class SessionResumeManager {
|
||||
LOGGER.error("[SessionResume] Error during deferred disconnect", e);
|
||||
}
|
||||
|
||||
// Clear the SSO ticket now that the grace period is truly over
|
||||
clearSsoTicket(habbo.getHabboInfo().getId());
|
||||
}
|
||||
|
||||
@@ -148,6 +133,38 @@ public class SessionResumeManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void applyPausedEffect(Habbo habbo) {
|
||||
int effectId = getPausedEffectId();
|
||||
if (effectId <= 0) return;
|
||||
try {
|
||||
RoomUnit unit = habbo.getRoomUnit();
|
||||
Room room = habbo.getHabboInfo() == null ? null : habbo.getHabboInfo().getCurrentRoom();
|
||||
if (unit == null || room == null) return;
|
||||
int endTimestamp = Emulator.getIntUnixTimestamp() + getGracePeriodSeconds() + 10;
|
||||
unit.setEffectId(effectId, endTimestamp);
|
||||
room.sendComposer(new RoomUserEffectComposer(unit).compose());
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[SessionResume] Failed to apply paused effect", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void restorePausedEffect(GhostSession ghost) {
|
||||
try {
|
||||
Habbo habbo = ghost.habbo;
|
||||
RoomUnit unit = habbo.getRoomUnit();
|
||||
Room room = habbo.getHabboInfo() == null ? null : habbo.getHabboInfo().getCurrentRoom();
|
||||
if (unit == null || room == null) return;
|
||||
|
||||
int pausedEffectId = getPausedEffectId();
|
||||
if (unit.getEffectId() == pausedEffectId) {
|
||||
unit.setEffectId(ghost.previousEffectId, ghost.previousEffectEnd);
|
||||
room.sendComposer(new RoomUserEffectComposer(unit).compose());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[SessionResume] Failed to restore previous effect", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearSsoTicket(int userId) {
|
||||
try (var connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) {
|
||||
@@ -163,11 +180,16 @@ public class SessionResumeManager {
|
||||
final Habbo habbo;
|
||||
final String ssoTicket;
|
||||
final ScheduledFuture<?> disposeFuture;
|
||||
final int previousEffectId;
|
||||
final int previousEffectEnd;
|
||||
|
||||
GhostSession(Habbo habbo, String ssoTicket, ScheduledFuture<?> disposeFuture) {
|
||||
GhostSession(Habbo habbo, String ssoTicket, ScheduledFuture<?> disposeFuture,
|
||||
int previousEffectId, int previousEffectEnd) {
|
||||
this.habbo = habbo;
|
||||
this.ssoTicket = ssoTicket;
|
||||
this.disposeFuture = disposeFuture;
|
||||
this.previousEffectId = previousEffectId;
|
||||
this.previousEffectEnd = previousEffectEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +219,10 @@ public class Messenger {
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
}
|
||||
|
||||
if (habbo.hasPermission(StaffChatBuddy.PERMISSION_KEY)) {
|
||||
this.friends.putIfAbsent(StaffChatBuddy.BUDDY_ID, new StaffChatBuddy(habbo.getHabboInfo().getId()));
|
||||
}
|
||||
}
|
||||
|
||||
public MessengerBuddy loadFriend(Habbo habbo, int userId) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.eu.habbo.habbohotel.messenger;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.commands.CommandHandler;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboGender;
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.friends.FriendChatMessageComposer;
|
||||
|
||||
public class StaffChatBuddy extends MessengerBuddy {
|
||||
public static final int BUDDY_ID = -1;
|
||||
public static final String PERMISSION_KEY = "acc_staff_chat";
|
||||
public static final String DISPLAY_NAME = "Staff Chat";
|
||||
public static final String DEFAULT_LOOK = "ADM";
|
||||
|
||||
public StaffChatBuddy(int userOne) {
|
||||
super(BUDDY_ID, DISPLAY_NAME, DEFAULT_LOOK, (short) 0, userOne);
|
||||
this.setOnline(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(Habbo from, String message) {
|
||||
if (from == null || message == null || message.isEmpty()) return;
|
||||
// Re-check permission so a staff member who was demoted mid-session
|
||||
// can no longer broadcast to the staff channel.
|
||||
if (!from.hasPermission(PERMISSION_KEY)) return;
|
||||
|
||||
if (message.charAt(0) == ':') {
|
||||
CommandHandler.handleCommand(from.getClient(), message);
|
||||
return;
|
||||
}
|
||||
|
||||
Message chatMessage = new Message(from.getHabboInfo().getId(), BUDDY_ID, message);
|
||||
Emulator.getGameServer().getGameClientManager().sendBroadcastResponse(
|
||||
new FriendChatMessageComposer(chatMessage, BUDDY_ID, from.getHabboInfo().getId()).compose(),
|
||||
PERMISSION_KEY,
|
||||
from.getClient());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(ServerMessage message) {
|
||||
message.appendInt(this.getId());
|
||||
message.appendString(this.getUsername());
|
||||
message.appendInt(this.getGender().equals(HabboGender.M) ? 0 : 1);
|
||||
message.appendBoolean(true); // online
|
||||
message.appendBoolean(false); // not in room
|
||||
message.appendString(this.getLook());
|
||||
message.appendInt(0); // category
|
||||
message.appendString(""); // motto
|
||||
message.appendString(""); // last seen
|
||||
message.appendString(""); // realname
|
||||
message.appendBoolean(true); // offline messaging supported
|
||||
message.appendBoolean(false);
|
||||
message.appendBoolean(false);
|
||||
message.appendShort(0); // relation
|
||||
}
|
||||
}
|
||||
@@ -15,25 +15,19 @@ import com.eu.habbo.habbohotel.items.interactions.games.tag.InteractionTagField;
|
||||
import com.eu.habbo.habbohotel.items.interactions.games.tag.InteractionTagPole;
|
||||
import com.eu.habbo.habbohotel.items.interactions.pets.*;
|
||||
import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectSendSignal;
|
||||
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredBlob;
|
||||
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFurniVariable;
|
||||
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRoomVariable;
|
||||
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUserVariable;
|
||||
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableEcho;
|
||||
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableReference;
|
||||
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableTextConnector;
|
||||
import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraContextVariable;
|
||||
import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport;
|
||||
import com.eu.habbo.habbohotel.items.interactions.wired.extra.*;
|
||||
import com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerReceiveSignal;
|
||||
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.HabboItem;
|
||||
import com.eu.habbo.habbohotel.users.HabboManager;
|
||||
import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport;
|
||||
import com.eu.habbo.habbohotel.wired.core.WiredManager;
|
||||
import com.eu.habbo.habbohotel.wired.core.WiredMovementPhysics;
|
||||
import com.eu.habbo.habbohotel.wired.tick.WiredTickable;
|
||||
import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer;
|
||||
import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer;
|
||||
import com.eu.habbo.messages.outgoing.rooms.items.*;
|
||||
import com.eu.habbo.plugin.Event;
|
||||
import com.eu.habbo.plugin.events.furniture.*;
|
||||
@@ -94,7 +88,7 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT * FROM items WHERE room_id = ?")) {
|
||||
"SELECT * FROM items WHERE room_id = ?")) {
|
||||
statement.setInt(1, this.room.getId());
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
@@ -106,8 +100,8 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
if (this.itemCount() > Room.MAXIMUM_FURNI) {
|
||||
LOGGER.error("Room ID: {} has exceeded the furniture limit ({} > {}).",
|
||||
this.room.getId(), this.itemCount(), Room.MAXIMUM_FURNI);
|
||||
LOGGER.error("Room ID: {} has exceeded the furniture limit ({} > {}).",
|
||||
this.room.getId(), this.itemCount(), Room.MAXIMUM_FURNI);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +110,7 @@ public class RoomItemManager {
|
||||
*/
|
||||
public void loadWiredData(Connection connection) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT id, wired_data FROM items WHERE room_id = ? AND wired_data<>''")) {
|
||||
"SELECT id, wired_data FROM items WHERE room_id = ? AND wired_data<>''")) {
|
||||
statement.setInt(1, this.room.getId());
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
@@ -274,7 +268,7 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
if (iterator.value().getBaseItem().getInteractionType().getType()
|
||||
== InteractionPostIt.class) {
|
||||
== InteractionPostIt.class) {
|
||||
items.add(iterator.value());
|
||||
}
|
||||
}
|
||||
@@ -359,7 +353,7 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
if (!(tile.x >= item.getX() && tile.x <= item.getX() + width - 1 && tile.y >= item.getY()
|
||||
&& tile.y <= item.getY() + length - 1)) {
|
||||
&& tile.y <= item.getY() + length - 1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -447,7 +441,7 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem)
|
||||
> item.getZ() + Item.getCurrentHeight(item)) {
|
||||
> item.getZ() + Item.getCurrentHeight(item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -516,7 +510,7 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem)
|
||||
> item.getZ() + Item.getCurrentHeight(item)) {
|
||||
> item.getZ() + Item.getCurrentHeight(item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -598,7 +592,7 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
if (lowestChair != null && lowestChair.getZ() + Item.getCurrentHeight(lowestChair)
|
||||
> item.getZ() + Item.getCurrentHeight(item)) {
|
||||
> item.getZ() + Item.getCurrentHeight(item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -647,7 +641,7 @@ public class RoomItemManager {
|
||||
this.furniOwnerNames.put(item.getUserId(), habbo.getUsername());
|
||||
} else {
|
||||
LOGGER.error("Failed to find username for item (ID: {}, UserID: {})",
|
||||
item.getId(), item.getUserId());
|
||||
item.getId(), item.getUserId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -665,7 +659,7 @@ public class RoomItemManager {
|
||||
if (specialTypes == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
boolean isWiredItem = false;
|
||||
|
||||
synchronized (specialTypes) {
|
||||
@@ -714,29 +708,29 @@ public class RoomItemManager {
|
||||
} else if (item instanceof InteractionPetTree) {
|
||||
specialTypes.addPetTree((InteractionPetTree) item);
|
||||
} else if (item instanceof InteractionMoodLight ||
|
||||
item instanceof InteractionPyramid ||
|
||||
item instanceof InteractionMusicDisc ||
|
||||
item instanceof InteractionBattleBanzaiSphere ||
|
||||
item instanceof InteractionTalkingFurniture ||
|
||||
item instanceof InteractionWater ||
|
||||
item instanceof InteractionWaterItem ||
|
||||
item instanceof InteractionMuteArea ||
|
||||
item instanceof InteractionBuildArea ||
|
||||
item instanceof InteractionTagPole ||
|
||||
item instanceof InteractionTagField ||
|
||||
item instanceof InteractionJukeBox ||
|
||||
item instanceof InteractionPetBreedingNest ||
|
||||
item instanceof InteractionBlackHole ||
|
||||
item instanceof InteractionWiredHighscore ||
|
||||
item instanceof InteractionStickyPole ||
|
||||
item instanceof WiredBlob ||
|
||||
item instanceof InteractionTent ||
|
||||
item instanceof InteractionSnowboardSlope ||
|
||||
item instanceof InteractionFireworks) {
|
||||
item instanceof InteractionPyramid ||
|
||||
item instanceof InteractionMusicDisc ||
|
||||
item instanceof InteractionBattleBanzaiSphere ||
|
||||
item instanceof InteractionTalkingFurniture ||
|
||||
item instanceof InteractionWater ||
|
||||
item instanceof InteractionWaterItem ||
|
||||
item instanceof InteractionMuteArea ||
|
||||
item instanceof InteractionBuildArea ||
|
||||
item instanceof InteractionTagPole ||
|
||||
item instanceof InteractionTagField ||
|
||||
item instanceof InteractionJukeBox ||
|
||||
item instanceof InteractionPetBreedingNest ||
|
||||
item instanceof InteractionBlackHole ||
|
||||
item instanceof InteractionWiredHighscore ||
|
||||
item instanceof InteractionStickyPole ||
|
||||
item instanceof WiredBlob ||
|
||||
item instanceof InteractionTent ||
|
||||
item instanceof InteractionSnowboardSlope ||
|
||||
item instanceof InteractionFireworks) {
|
||||
specialTypes.addUndefined(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Invalidate wired cache when wired items are added
|
||||
if (isWiredItem) {
|
||||
WiredManager.invalidateRoom(this.room);
|
||||
@@ -810,7 +804,7 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
this.room.getFurniVariableManager().removeAssignmentsForFurni(item.getId());
|
||||
|
||||
|
||||
boolean isWiredItem = false;
|
||||
|
||||
// Unregister from tick service for time-based wired triggers (new 50ms tick system)
|
||||
@@ -822,53 +816,53 @@ public class RoomItemManager {
|
||||
specialTypes.removeCycleTask((ICycleable) item);
|
||||
}
|
||||
|
||||
if (item instanceof InteractionBattleBanzaiTeleporter) {
|
||||
specialTypes.removeBanzaiTeleporter((InteractionBattleBanzaiTeleporter) item);
|
||||
} else if (item instanceof InteractionWiredTrigger) {
|
||||
specialTypes.removeTrigger((InteractionWiredTrigger) item);
|
||||
isWiredItem = true;
|
||||
} else if (item instanceof InteractionWiredEffect) {
|
||||
specialTypes.removeEffect((InteractionWiredEffect) item);
|
||||
isWiredItem = true;
|
||||
} else if (item instanceof InteractionWiredCondition) {
|
||||
specialTypes.removeCondition((InteractionWiredCondition) item);
|
||||
isWiredItem = true;
|
||||
} else if (item instanceof InteractionWiredExtra) {
|
||||
boolean removedContextDefinition = false;
|
||||
boolean removedVariableTextConnector = false;
|
||||
if (item instanceof WiredExtraUserVariable) {
|
||||
this.room.getUserVariableManager().removeDefinition(item.getId());
|
||||
} else if (item instanceof WiredExtraFurniVariable) {
|
||||
this.room.getFurniVariableManager().removeDefinition(item.getId());
|
||||
} else if (item instanceof WiredExtraRoomVariable) {
|
||||
this.room.getRoomVariableManager().removeDefinition(item.getId());
|
||||
} else if (item instanceof WiredExtraContextVariable) {
|
||||
removedContextDefinition = true;
|
||||
} else if (item instanceof WiredExtraVariableTextConnector) {
|
||||
removedVariableTextConnector = true;
|
||||
} else if (item instanceof WiredExtraVariableReference) {
|
||||
if (((WiredExtraVariableReference) item).isRoomReference()) {
|
||||
this.room.getRoomVariableManager().removeDefinition(item.getId());
|
||||
} else {
|
||||
this.room.getUserVariableManager().removeDefinition(item.getId());
|
||||
}
|
||||
} else if (item instanceof WiredExtraVariableEcho) {
|
||||
WiredExtraVariableEcho echo = (WiredExtraVariableEcho) item;
|
||||
if (item instanceof InteractionBattleBanzaiTeleporter) {
|
||||
specialTypes.removeBanzaiTeleporter((InteractionBattleBanzaiTeleporter) item);
|
||||
} else if (item instanceof InteractionWiredTrigger) {
|
||||
specialTypes.removeTrigger((InteractionWiredTrigger) item);
|
||||
isWiredItem = true;
|
||||
} else if (item instanceof InteractionWiredEffect) {
|
||||
specialTypes.removeEffect((InteractionWiredEffect) item);
|
||||
isWiredItem = true;
|
||||
} else if (item instanceof InteractionWiredCondition) {
|
||||
specialTypes.removeCondition((InteractionWiredCondition) item);
|
||||
isWiredItem = true;
|
||||
} else if (item instanceof InteractionWiredExtra) {
|
||||
boolean removedContextDefinition = false;
|
||||
boolean removedVariableTextConnector = false;
|
||||
if (item instanceof WiredExtraUserVariable) {
|
||||
this.room.getUserVariableManager().removeDefinition(item.getId());
|
||||
} else if (item instanceof WiredExtraFurniVariable) {
|
||||
this.room.getFurniVariableManager().removeDefinition(item.getId());
|
||||
} else if (item instanceof WiredExtraRoomVariable) {
|
||||
this.room.getRoomVariableManager().removeDefinition(item.getId());
|
||||
} else if (item instanceof WiredExtraContextVariable) {
|
||||
removedContextDefinition = true;
|
||||
} else if (item instanceof WiredExtraVariableTextConnector) {
|
||||
removedVariableTextConnector = true;
|
||||
} else if (item instanceof WiredExtraVariableReference) {
|
||||
if (((WiredExtraVariableReference) item).isRoomReference()) {
|
||||
this.room.getRoomVariableManager().removeDefinition(item.getId());
|
||||
} else {
|
||||
this.room.getUserVariableManager().removeDefinition(item.getId());
|
||||
}
|
||||
} else if (item instanceof WiredExtraVariableEcho) {
|
||||
WiredExtraVariableEcho echo = (WiredExtraVariableEcho) item;
|
||||
|
||||
if (echo.isRoomEcho()) {
|
||||
this.room.getRoomVariableManager().removeDefinition(item.getId());
|
||||
} else if (echo.isFurniEcho()) {
|
||||
this.room.getFurniVariableManager().removeDefinition(item.getId());
|
||||
} else {
|
||||
this.room.getUserVariableManager().removeDefinition(item.getId());
|
||||
}
|
||||
}
|
||||
specialTypes.removeExtra((InteractionWiredExtra) item);
|
||||
if (removedContextDefinition || removedVariableTextConnector) {
|
||||
WiredContextVariableSupport.broadcastDefinitions(this.room);
|
||||
}
|
||||
isWiredItem = true;
|
||||
} else if (item instanceof InteractionRoller) {
|
||||
if (echo.isRoomEcho()) {
|
||||
this.room.getRoomVariableManager().removeDefinition(item.getId());
|
||||
} else if (echo.isFurniEcho()) {
|
||||
this.room.getFurniVariableManager().removeDefinition(item.getId());
|
||||
} else {
|
||||
this.room.getUserVariableManager().removeDefinition(item.getId());
|
||||
}
|
||||
}
|
||||
specialTypes.removeExtra((InteractionWiredExtra) item);
|
||||
if (removedContextDefinition || removedVariableTextConnector) {
|
||||
WiredContextVariableSupport.broadcastDefinitions(this.room);
|
||||
}
|
||||
isWiredItem = true;
|
||||
} else if (item instanceof InteractionRoller) {
|
||||
specialTypes.removeRoller((InteractionRoller) item);
|
||||
} else if (item instanceof InteractionGameScoreboard) {
|
||||
specialTypes.removeScoreboard((InteractionGameScoreboard) item);
|
||||
@@ -889,26 +883,26 @@ public class RoomItemManager {
|
||||
} else if (item instanceof InteractionPetTree) {
|
||||
specialTypes.removePetTree((InteractionPetTree) item);
|
||||
} else if (item instanceof InteractionMoodLight ||
|
||||
item instanceof InteractionPyramid ||
|
||||
item instanceof InteractionMusicDisc ||
|
||||
item instanceof InteractionBattleBanzaiSphere ||
|
||||
item instanceof InteractionTalkingFurniture ||
|
||||
item instanceof InteractionWaterItem ||
|
||||
item instanceof InteractionWater ||
|
||||
item instanceof InteractionMuteArea ||
|
||||
item instanceof InteractionTagPole ||
|
||||
item instanceof InteractionTagField ||
|
||||
item instanceof InteractionJukeBox ||
|
||||
item instanceof InteractionPetBreedingNest ||
|
||||
item instanceof InteractionBlackHole ||
|
||||
item instanceof InteractionWiredHighscore ||
|
||||
item instanceof InteractionStickyPole ||
|
||||
item instanceof WiredBlob ||
|
||||
item instanceof InteractionTent ||
|
||||
item instanceof InteractionSnowboardSlope) {
|
||||
item instanceof InteractionPyramid ||
|
||||
item instanceof InteractionMusicDisc ||
|
||||
item instanceof InteractionBattleBanzaiSphere ||
|
||||
item instanceof InteractionTalkingFurniture ||
|
||||
item instanceof InteractionWaterItem ||
|
||||
item instanceof InteractionWater ||
|
||||
item instanceof InteractionMuteArea ||
|
||||
item instanceof InteractionTagPole ||
|
||||
item instanceof InteractionTagField ||
|
||||
item instanceof InteractionJukeBox ||
|
||||
item instanceof InteractionPetBreedingNest ||
|
||||
item instanceof InteractionBlackHole ||
|
||||
item instanceof InteractionWiredHighscore ||
|
||||
item instanceof InteractionStickyPole ||
|
||||
item instanceof WiredBlob ||
|
||||
item instanceof InteractionTent ||
|
||||
item instanceof InteractionSnowboardSlope) {
|
||||
specialTypes.removeUndefined(item);
|
||||
}
|
||||
|
||||
|
||||
// Invalidate wired cache when wired items are removed
|
||||
if (isWiredItem || cleanedSignalAntennaReferences) {
|
||||
WiredManager.invalidateRoom(this.room);
|
||||
@@ -936,9 +930,9 @@ public class RoomItemManager {
|
||||
if (item.getBaseItem().getType() == FurnitureType.FLOOR) {
|
||||
this.room.sendComposer(new FloorItemUpdateComposer(item).compose());
|
||||
this.room.updateTiles(this.room.getLayout()
|
||||
.getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()),
|
||||
item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
|
||||
item.getRotation()));
|
||||
.getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()),
|
||||
item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
|
||||
item.getRotation()));
|
||||
} else if (item.getBaseItem().getType() == FurnitureType.WALL) {
|
||||
this.room.sendComposer(new WallItemUpdateComposer(item).compose());
|
||||
}
|
||||
@@ -963,9 +957,9 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
this.room.updateTiles(this.room.getLayout()
|
||||
.getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()),
|
||||
item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
|
||||
item.getRotation()));
|
||||
.getTilesAt(this.room.getLayout().getTile(item.getX(), item.getY()),
|
||||
item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
|
||||
item.getRotation()));
|
||||
|
||||
if (item instanceof InteractionMultiHeight) {
|
||||
((InteractionMultiHeight) item).updateUnitsOnItem(this.room);
|
||||
@@ -1032,7 +1026,7 @@ public class RoomItemManager {
|
||||
|
||||
if (Emulator.getPluginManager().isRegistered(FurniturePickedUpEvent.class, true)) {
|
||||
FurniturePickedUpEvent event = Emulator.getPluginManager()
|
||||
.fireEvent(new FurniturePickedUpEvent(item, picker));
|
||||
.fireEvent(new FurniturePickedUpEvent(item, picker));
|
||||
|
||||
if (event.isCancelled()) {
|
||||
return;
|
||||
@@ -1060,10 +1054,10 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
THashSet<RoomTile> updatedTiles = this.room.getLayout().getTilesAt(
|
||||
this.room.getLayout().getTile(item.getX(), item.getY()),
|
||||
item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(),
|
||||
item.getRotation());
|
||||
this.room.getLayout().getTile(item.getX(), item.getY()),
|
||||
item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(),
|
||||
item.getRotation());
|
||||
this.room.updateTiles(updatedTiles);
|
||||
|
||||
for (RoomTile tile : updatedTiles) {
|
||||
@@ -1114,6 +1108,7 @@ public class RoomItemManager {
|
||||
if (habbo != null && !inventoryItems.isEmpty()) {
|
||||
habbo.getInventory().getItemsComponent().addItems(inventoryItems);
|
||||
habbo.getClient().sendResponse(new AddHabboItemComposer(inventoryItems));
|
||||
habbo.getClient().sendResponse(new InventoryRefreshComposer());
|
||||
}
|
||||
|
||||
for (HabboItem i : items) {
|
||||
@@ -1160,7 +1155,7 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
userItemsMap.computeIfAbsent(iterator.value().getUserId(), k -> new THashSet<>())
|
||||
.add(iterator.value());
|
||||
.add(iterator.value());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1182,6 +1177,7 @@ public class RoomItemManager {
|
||||
if (user != null && !inventoryItems.isEmpty()) {
|
||||
user.getInventory().getItemsComponent().addItems(inventoryItems);
|
||||
user.getClient().sendResponse(new AddHabboItemComposer(inventoryItems));
|
||||
user.getClient().sendResponse(new InventoryRefreshComposer());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1222,7 +1218,7 @@ public class RoomItemManager {
|
||||
for (short y = 0; y < item.getBaseItem().getLength(); y++) {
|
||||
for (short x = 0; x < item.getBaseItem().getWidth(); x++) {
|
||||
RoomTile tile = this.room.getLayout().getTile(
|
||||
(short) (item.getX() + x), (short) (item.getY() + y));
|
||||
(short) (item.getX() + x), (short) (item.getY() + y));
|
||||
|
||||
if (tile != null) {
|
||||
lockedTiles.add(tile);
|
||||
@@ -1233,7 +1229,7 @@ public class RoomItemManager {
|
||||
for (short y = 0; y < item.getBaseItem().getWidth(); y++) {
|
||||
for (short x = 0; x < item.getBaseItem().getLength(); x++) {
|
||||
RoomTile tile = this.room.getLayout().getTile(
|
||||
(short) (item.getX() + x), (short) (item.getY() + y));
|
||||
(short) (item.getX() + x), (short) (item.getY() + y));
|
||||
|
||||
if (tile != null) {
|
||||
lockedTiles.add(tile);
|
||||
@@ -1324,8 +1320,8 @@ public class RoomItemManager {
|
||||
|
||||
rotation %= 8;
|
||||
if (this.room.hasRights(habbo) || this.room.getGuildRightLevel(habbo)
|
||||
.isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || habbo.hasPermission(
|
||||
Permission.ACC_MOVEROTATE) || BuildersClubRoomSupport.canPlaceInRoom(habbo, this.room)) {
|
||||
.isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || habbo.hasPermission(
|
||||
Permission.ACC_MOVEROTATE) || BuildersClubRoomSupport.canPlaceInRoom(habbo, this.room)) {
|
||||
return FurnitureMovementError.NONE;
|
||||
}
|
||||
|
||||
@@ -1334,10 +1330,10 @@ public class RoomItemManager {
|
||||
|
||||
if (rentSpace != null) {
|
||||
if (!RoomLayout.squareInSquare(RoomLayout.getRectangle(rentSpace.getX(), rentSpace.getY(),
|
||||
rentSpace.getBaseItem().getWidth(), rentSpace.getBaseItem().getLength(),
|
||||
rentSpace.getRotation()),
|
||||
RoomLayout.getRectangle(tile.x, tile.y, item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(), rotation))) {
|
||||
rentSpace.getBaseItem().getWidth(), rentSpace.getBaseItem().getLength(),
|
||||
rentSpace.getRotation()),
|
||||
RoomLayout.getRectangle(tile.x, tile.y, item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(), rotation))) {
|
||||
return FurnitureMovementError.NO_RIGHTS;
|
||||
} else {
|
||||
return FurnitureMovementError.NONE;
|
||||
@@ -1347,7 +1343,7 @@ public class RoomItemManager {
|
||||
|
||||
for (HabboItem area : this.room.getRoomSpecialTypes().getItemsOfType(InteractionBuildArea.class)) {
|
||||
if (((InteractionBuildArea) area).inSquare(tile) && ((InteractionBuildArea) area).isBuilder(
|
||||
habbo.getHabboInfo().getUsername())) {
|
||||
habbo.getHabboInfo().getUsername())) {
|
||||
return FurnitureMovementError.NONE;
|
||||
}
|
||||
}
|
||||
@@ -1438,14 +1434,14 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(), rotation);
|
||||
item.getBaseItem().getLength(), rotation);
|
||||
for (RoomTile t : occupiedTiles) {
|
||||
if (t.state == RoomTileState.INVALID) {
|
||||
return FurnitureMovementError.INVALID_MOVE;
|
||||
}
|
||||
if (!Emulator.getConfig().getBoolean("wired.place.under", false) || (
|
||||
Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable()
|
||||
&& !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) {
|
||||
Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable()
|
||||
&& !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) {
|
||||
if (checkForUnits && this.room.hasHabbosAt(t.x, t.y)) {
|
||||
return FurnitureMovementError.TILE_HAS_HABBOS;
|
||||
}
|
||||
@@ -1490,7 +1486,7 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(), rotation);
|
||||
item.getBaseItem().getLength(), rotation);
|
||||
for (RoomTile t : occupiedTiles) {
|
||||
if (t.state == RoomTileState.INVALID) {
|
||||
return FurnitureMovementError.INVALID_MOVE;
|
||||
@@ -1542,7 +1538,7 @@ public class RoomItemManager {
|
||||
boolean pluginHelper = false;
|
||||
if (Emulator.getPluginManager().isRegistered(FurniturePlacedEvent.class, true)) {
|
||||
FurniturePlacedEvent event = Emulator.getPluginManager()
|
||||
.fireEvent(new FurniturePlacedEvent(item, owner, tile));
|
||||
.fireEvent(new FurniturePlacedEvent(item, owner, tile));
|
||||
|
||||
if (event.isCancelled()) {
|
||||
return FurnitureMovementError.CANCEL_PLUGIN_PLACE;
|
||||
@@ -1553,7 +1549,7 @@ public class RoomItemManager {
|
||||
|
||||
RoomLayout layout = this.room.getLayout();
|
||||
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(), rotation);
|
||||
item.getBaseItem().getLength(), rotation);
|
||||
|
||||
FurnitureMovementError fits = furnitureFitsAt(tile, item, rotation);
|
||||
|
||||
@@ -1572,7 +1568,7 @@ public class RoomItemManager {
|
||||
|
||||
if (Emulator.getPluginManager().isRegistered(FurnitureBuildheightEvent.class, true)) {
|
||||
FurnitureBuildheightEvent event = Emulator.getPluginManager()
|
||||
.fireEvent(new FurnitureBuildheightEvent(item, owner, 0.00, height));
|
||||
.fireEvent(new FurnitureBuildheightEvent(item, owner, 0.00, height));
|
||||
if (event.hasChangedHeight()) {
|
||||
height = layout.getHeightAtSquare(tile.x, tile.y) + event.getUpdatedHeight();
|
||||
}
|
||||
@@ -1592,7 +1588,7 @@ public class RoomItemManager {
|
||||
item.onPlace(this.room);
|
||||
this.room.updateTiles(occupiedTiles);
|
||||
this.room.sendComposer(
|
||||
new AddFloorItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose());
|
||||
new AddFloorItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose());
|
||||
|
||||
if (RoomConfInvisSupport.isControllerItem(item) || RoomConfInvisSupport.isTarget(item)) {
|
||||
RoomConfInvisSupport.sendState(this.room);
|
||||
@@ -1620,7 +1616,7 @@ public class RoomItemManager {
|
||||
*/
|
||||
public FurnitureMovementError placeWallFurniAt(HabboItem item, String wallPosition, Habbo owner) {
|
||||
if (!(this.room.hasRights(owner) || this.room.getGuildRightLevel(owner)
|
||||
.isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || BuildersClubRoomSupport.canPlaceInRoom(owner, this.room))) {
|
||||
.isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || BuildersClubRoomSupport.canPlaceInRoom(owner, this.room))) {
|
||||
return FurnitureMovementError.NO_RIGHTS;
|
||||
}
|
||||
|
||||
@@ -1638,7 +1634,7 @@ public class RoomItemManager {
|
||||
this.furniOwnerNames.put(item.getUserId(), this.resolveOwnerName(item, owner));
|
||||
}
|
||||
this.room.sendComposer(
|
||||
new AddWallItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose());
|
||||
new AddWallItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose());
|
||||
item.needsUpdate(true);
|
||||
this.addHabboItem(item);
|
||||
item.setRoomId(this.room.getId());
|
||||
@@ -1989,7 +1985,7 @@ public class RoomItemManager {
|
||||
boolean pluginHelper = false;
|
||||
if (Emulator.getPluginManager().isRegistered(FurnitureMovedEvent.class, true)) {
|
||||
FurnitureMovedEvent event = Emulator.getPluginManager()
|
||||
.fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile));
|
||||
.fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile));
|
||||
if (event.isCancelled()) {
|
||||
return FurnitureMovementError.CANCEL_PLUGIN_MOVE;
|
||||
}
|
||||
@@ -2002,9 +1998,9 @@ public class RoomItemManager {
|
||||
|
||||
// Check if can be placed at new position
|
||||
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(), rotation);
|
||||
item.getBaseItem().getLength(), rotation);
|
||||
THashSet<RoomTile> newOccupiedTiles = layout.getTilesAt(tile,
|
||||
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation);
|
||||
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation);
|
||||
|
||||
HabboItem topItem = this.getTopItemAt(occupiedTiles, null);
|
||||
|
||||
@@ -2013,15 +2009,15 @@ public class RoomItemManager {
|
||||
for (RoomTile t : occupiedTiles) {
|
||||
HabboItem tileTopItem = this.getTopItemAt(t.x, t.y);
|
||||
if (!magicTile && ((tileTopItem != null && tileTopItem != item ? (
|
||||
t.state.equals(RoomTileState.INVALID) || !t.getAllowStack()
|
||||
t.state.equals(RoomTileState.INVALID) || !t.getAllowStack()
|
||||
|| !tileTopItem.getBaseItem().allowStack())
|
||||
: this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) {
|
||||
: this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) {
|
||||
return FurnitureMovementError.CANT_STACK;
|
||||
}
|
||||
|
||||
if (!Emulator.getConfig().getBoolean("wired.place.under", false) || (
|
||||
Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable()
|
||||
&& !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) {
|
||||
Emulator.getConfig().getBoolean("wired.place.under", false) && !item.isWalkable()
|
||||
&& !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay())) {
|
||||
if (checkForUnits) {
|
||||
if (!magicTile && this.room.hasHabbosAt(t.x, t.y)) {
|
||||
return FurnitureMovementError.TILE_HAS_HABBOS;
|
||||
@@ -2048,8 +2044,8 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
THashSet<RoomTile> oldOccupiedTiles = layout.getTilesAt(
|
||||
layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(), item.getRotation());
|
||||
layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(), item.getRotation());
|
||||
|
||||
int oldRotation = item.getRotation();
|
||||
|
||||
@@ -2066,9 +2062,9 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
if ((stackHelper == null && topItem != null && topItem != item && !topItem.getBaseItem()
|
||||
.allowStack()) || (topItem != null && topItem != item
|
||||
&& topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item)
|
||||
> Room.MAXIMUM_FURNI_HEIGHT)) {
|
||||
.allowStack()) || (topItem != null && topItem != item
|
||||
&& topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item)
|
||||
> Room.MAXIMUM_FURNI_HEIGHT)) {
|
||||
item.setRotation(oldRotation);
|
||||
return FurnitureMovementError.CANT_STACK;
|
||||
}
|
||||
@@ -2117,7 +2113,7 @@ public class RoomItemManager {
|
||||
|
||||
if (Emulator.getPluginManager().isRegistered(FurnitureBuildheightEvent.class, true)) {
|
||||
FurnitureBuildheightEvent event = Emulator.getPluginManager()
|
||||
.fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height));
|
||||
.fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height));
|
||||
if (event.hasChangedHeight()) {
|
||||
height = layout.getHeightAtSquare(tile.x, tile.y) + event.getUpdatedHeight();
|
||||
pluginHeight = true;
|
||||
@@ -2138,7 +2134,7 @@ public class RoomItemManager {
|
||||
if (item.getZ() > Room.MAXIMUM_FURNI_HEIGHT) {
|
||||
item.setZ(Room.MAXIMUM_FURNI_HEIGHT);
|
||||
}
|
||||
|
||||
|
||||
// Update wired spatial index and invalidate cache when wired items are moved
|
||||
if (item instanceof InteractionWiredTrigger) {
|
||||
this.room.getRoomSpecialTypes().updateTriggerLocation((InteractionWiredTrigger) item, oldLocation.x, oldLocation.y);
|
||||
@@ -2198,7 +2194,7 @@ public class RoomItemManager {
|
||||
boolean pluginHelper = false;
|
||||
if (Emulator.getPluginManager().isRegistered(FurnitureMovedEvent.class, true)) {
|
||||
FurnitureMovedEvent event = Emulator.getPluginManager()
|
||||
.fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile));
|
||||
.fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile));
|
||||
if (event.isCancelled()) {
|
||||
return FurnitureMovementError.CANCEL_PLUGIN_MOVE;
|
||||
}
|
||||
@@ -2210,9 +2206,9 @@ public class RoomItemManager {
|
||||
HabboItem stackHelper = this.findStackHeightHelperAt(tile, item);
|
||||
|
||||
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(), rotation);
|
||||
item.getBaseItem().getLength(), rotation);
|
||||
THashSet<RoomTile> newOccupiedTiles = layout.getTilesAt(tile,
|
||||
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation);
|
||||
item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation);
|
||||
|
||||
HabboItem topItem = this.getTopPhysicsItemAt(occupiedTiles, null, physics);
|
||||
|
||||
@@ -2221,9 +2217,9 @@ public class RoomItemManager {
|
||||
for (RoomTile t : occupiedTiles) {
|
||||
HabboItem tileTopItem = this.getTopPhysicsItemAt(t.x, t.y, item, physics);
|
||||
if (!magicTile && ((tileTopItem != null && tileTopItem != item ? (
|
||||
t.state.equals(RoomTileState.INVALID) || !t.getAllowStack()
|
||||
t.state.equals(RoomTileState.INVALID) || !t.getAllowStack()
|
||||
|| !tileTopItem.getBaseItem().allowStack())
|
||||
: this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) {
|
||||
: this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) {
|
||||
return FurnitureMovementError.CANT_STACK;
|
||||
}
|
||||
|
||||
@@ -2251,8 +2247,8 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
THashSet<RoomTile> oldOccupiedTiles = layout.getTilesAt(
|
||||
layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(), item.getRotation());
|
||||
layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(), item.getRotation());
|
||||
|
||||
int oldRotation = item.getRotation();
|
||||
|
||||
@@ -2269,9 +2265,9 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
if ((stackHelper == null && topItem != null && topItem != item && !topItem.getBaseItem()
|
||||
.allowStack()) || (topItem != null && topItem != item
|
||||
&& topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item)
|
||||
> Room.MAXIMUM_FURNI_HEIGHT)) {
|
||||
.allowStack()) || (topItem != null && topItem != item
|
||||
&& topItem.getZ() + Item.getCurrentHeight(topItem) + Item.getCurrentHeight(item)
|
||||
> Room.MAXIMUM_FURNI_HEIGHT)) {
|
||||
item.setRotation(oldRotation);
|
||||
return FurnitureMovementError.CANT_STACK;
|
||||
}
|
||||
@@ -2319,7 +2315,7 @@ public class RoomItemManager {
|
||||
|
||||
if (Emulator.getPluginManager().isRegistered(FurnitureBuildheightEvent.class, true)) {
|
||||
FurnitureBuildheightEvent event = Emulator.getPluginManager()
|
||||
.fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height));
|
||||
.fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height));
|
||||
if (event.hasChangedHeight()) {
|
||||
height = layout.getHeightAtSquare(tile.x, tile.y) + event.getUpdatedHeight();
|
||||
pluginHeight = true;
|
||||
@@ -2391,10 +2387,10 @@ public class RoomItemManager {
|
||||
boolean magicTile = this.isStackPlacementBypassItem(item);
|
||||
|
||||
RoomLayout layout = this.room.getLayout();
|
||||
|
||||
|
||||
// Check if can be placed at new position
|
||||
THashSet<RoomTile> occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(),
|
||||
item.getBaseItem().getLength(), rotation);
|
||||
item.getBaseItem().getLength(), rotation);
|
||||
|
||||
java.util.List<Pair<RoomTile, THashSet<HabboItem>>> tileFurniList = new java.util.ArrayList<>();
|
||||
for (RoomTile t : occupiedTiles) {
|
||||
@@ -2438,8 +2434,8 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
return !item.isWalkable()
|
||||
&& !item.getBaseItem().allowSit()
|
||||
&& !item.getBaseItem().allowLay();
|
||||
&& !item.getBaseItem().allowSit()
|
||||
&& !item.getBaseItem().allowLay();
|
||||
}
|
||||
|
||||
private FurnitureMovementError getPhysicsUnitCollision(RoomTile tile, WiredMovementPhysics physics) {
|
||||
@@ -2515,7 +2511,7 @@ public class RoomItemManager {
|
||||
|
||||
for (HabboItem item : this.getPhysicsItemsAt(tile, exclude, physics)) {
|
||||
if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem)
|
||||
> item.getZ() + Item.getCurrentHeight(item)) {
|
||||
> item.getZ() + Item.getCurrentHeight(item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2539,7 +2535,7 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem)
|
||||
> topItem.getZ() + Item.getCurrentHeight(topItem)) {
|
||||
> topItem.getZ() + Item.getCurrentHeight(topItem)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ public class HabboInfo implements Runnable {
|
||||
private int InfostandBg;
|
||||
private int InfostandStand;
|
||||
private int InfostandOverlay;
|
||||
private int InfostandCardBg;
|
||||
private int loadingRoom;
|
||||
private Room currentRoom;
|
||||
private String roomEntryMethod = "door";
|
||||
@@ -91,6 +92,7 @@ public class HabboInfo implements Runnable {
|
||||
this.InfostandBg = set.getInt("background_id");
|
||||
this.InfostandStand = set.getInt("background_stand_id");
|
||||
this.InfostandOverlay = set.getInt("background_overlay_id");
|
||||
this.InfostandCardBg = set.getInt("background_card_id");
|
||||
this.currentRoom = null;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
@@ -290,6 +292,14 @@ public class HabboInfo implements Runnable {
|
||||
public void setInfostandOverlay(int infostandOverlay) {
|
||||
InfostandOverlay = infostandOverlay;
|
||||
}
|
||||
|
||||
public int getInfostandCardBg() {
|
||||
return InfostandCardBg;
|
||||
}
|
||||
|
||||
public void setInfostandCardBg(int infostandCardBg) {
|
||||
InfostandCardBg = infostandCardBg;
|
||||
}
|
||||
public Rank getRank() {
|
||||
return this.rank;
|
||||
}
|
||||
@@ -577,7 +587,7 @@ public class HabboInfo implements Runnable {
|
||||
|
||||
try {
|
||||
SqlQueries.update(
|
||||
"UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ? WHERE id = ?",
|
||||
"UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ?, background_card_id = ? WHERE id = ?",
|
||||
this.motto,
|
||||
this.online ? "1" : "0",
|
||||
this.look,
|
||||
@@ -593,6 +603,7 @@ public class HabboInfo implements Runnable {
|
||||
this.InfostandBg,
|
||||
this.InfostandStand,
|
||||
this.InfostandOverlay,
|
||||
this.InfostandCardBg,
|
||||
this.id);
|
||||
} catch (SqlQueries.DataAccessException e) {
|
||||
LOGGER.error("Caught SQL exception", e);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.eu.habbo.habbohotel.users.custombadge;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class CustomBadge {
|
||||
|
||||
private final int id;
|
||||
private final int userId;
|
||||
private final String badgeId;
|
||||
private String badgeName;
|
||||
private String badgeDescription;
|
||||
private final int dateCreated;
|
||||
private int dateEdit;
|
||||
|
||||
public CustomBadge(ResultSet set) throws SQLException {
|
||||
this.id = set.getInt("id");
|
||||
this.userId = set.getInt("user_id");
|
||||
this.badgeId = set.getString("badge_id");
|
||||
this.badgeName = set.getString("badge_name");
|
||||
this.badgeDescription = set.getString("badge_description");
|
||||
this.dateCreated = set.getInt("date_created");
|
||||
this.dateEdit = set.getInt("date_edit");
|
||||
}
|
||||
|
||||
public CustomBadge(int id, int userId, String badgeId, String badgeName, String badgeDescription, int dateCreated, int dateEdit) {
|
||||
this.id = id;
|
||||
this.userId = userId;
|
||||
this.badgeId = badgeId;
|
||||
this.badgeName = badgeName;
|
||||
this.badgeDescription = badgeDescription;
|
||||
this.dateCreated = dateCreated;
|
||||
this.dateEdit = dateEdit;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public int getUserId() {
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
public String getBadgeId() {
|
||||
return this.badgeId;
|
||||
}
|
||||
|
||||
public String getBadgeName() {
|
||||
return this.badgeName;
|
||||
}
|
||||
|
||||
public String getBadgeDescription() {
|
||||
return this.badgeDescription;
|
||||
}
|
||||
|
||||
public int getDateCreated() {
|
||||
return this.dateCreated;
|
||||
}
|
||||
|
||||
public int getDateEdit() {
|
||||
return this.dateEdit;
|
||||
}
|
||||
|
||||
public void setBadgeName(String badgeName) {
|
||||
this.badgeName = badgeName;
|
||||
}
|
||||
|
||||
public void setBadgeDescription(String badgeDescription) {
|
||||
this.badgeDescription = badgeDescription;
|
||||
}
|
||||
|
||||
public void setDateEdit(int dateEdit) {
|
||||
this.dateEdit = dateEdit;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.eu.habbo.habbohotel.users.custombadge;
|
||||
|
||||
public class CustomBadgeException extends Exception {
|
||||
|
||||
private final String code;
|
||||
|
||||
public CustomBadgeException(String code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return this.code;
|
||||
}
|
||||
}
|
||||
+588
@@ -0,0 +1,588 @@
|
||||
package com.eu.habbo.habbohotel.users.custombadge;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboBadge;
|
||||
import com.eu.habbo.habbohotel.users.inventory.BadgesComponent;
|
||||
import com.eu.habbo.messages.outgoing.inventory.InventoryBadgesComposer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.IndexColorModel;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.SecureRandom;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class CustomBadgeManager {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CustomBadgeManager.class);
|
||||
|
||||
public static final int MAX_PER_USER = 5;
|
||||
public static final int BADGE_WIDTH = 40;
|
||||
public static final int BADGE_HEIGHT = 40;
|
||||
public static final int MAX_BADGE_SIZE_BYTES = 40960;
|
||||
|
||||
private static final int RANDOM_SUFFIX_LENGTH = 5;
|
||||
private static final char[] RANDOM_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
|
||||
private static final Pattern BADGE_ID_PATTERN = Pattern.compile("^CUST[A-Z0-9]{" + RANDOM_SUFFIX_LENGTH + "}-\\d+$");
|
||||
|
||||
private static final byte[] PNG_MAGIC = { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
|
||||
|
||||
private static final int RATE_LIMIT_OPS = 5;
|
||||
private static final long RATE_LIMIT_WINDOW_MS = 60_000L;
|
||||
|
||||
private final SecureRandom random = new SecureRandom();
|
||||
private final Map<Integer, long[]> rateBuckets = new ConcurrentHashMap<>();
|
||||
private final Map<String, BadgeText> textCache = new ConcurrentHashMap<>();
|
||||
private final java.util.concurrent.atomic.AtomicLong textCacheVersion = new java.util.concurrent.atomic.AtomicLong();
|
||||
|
||||
private volatile CustomBadgeSettings settings;
|
||||
|
||||
public CustomBadgeManager() {
|
||||
this.reload();
|
||||
}
|
||||
|
||||
public static final class BadgeText {
|
||||
public final String name;
|
||||
public final String description;
|
||||
public BadgeText(String name, String description) {
|
||||
this.name = name == null ? "" : name;
|
||||
this.description = description == null ? "" : description;
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, BadgeText> getTextCache() {
|
||||
return java.util.Collections.unmodifiableMap(this.textCache);
|
||||
}
|
||||
|
||||
public long getTextCacheVersion() {
|
||||
return this.textCacheVersion.get();
|
||||
}
|
||||
|
||||
private void loadTextCache() {
|
||||
Map<String, BadgeText> next = new java.util.HashMap<>();
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT `badge_id`, `badge_name`, `badge_description` FROM `user_custom_badge`")) {
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
while (resultSet.next()) {
|
||||
next.put(resultSet.getString("badge_id"),
|
||||
new BadgeText(
|
||||
resultSet.getString("badge_name"),
|
||||
resultSet.getString("badge_description")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("CustomBadgeManager -> Failed to load badge text cache.", e);
|
||||
return;
|
||||
}
|
||||
this.textCache.clear();
|
||||
this.textCache.putAll(next);
|
||||
this.textCacheVersion.incrementAndGet();
|
||||
LOGGER.info("CustomBadgeManager -> loaded {} custom badge texts into memory.", next.size());
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT `badge_path`, `badge_url`, `price_badge`, `currency_type` FROM `users_custom_badge_settings` ORDER BY `id` ASC LIMIT 1")) {
|
||||
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
if (resultSet.next()) {
|
||||
this.settings = new CustomBadgeSettings(
|
||||
resultSet.getString("badge_path"),
|
||||
resultSet.getString("badge_url"),
|
||||
resultSet.getInt("price_badge"),
|
||||
resultSet.getInt("currency_type"));
|
||||
} else {
|
||||
this.settings = new CustomBadgeSettings(
|
||||
"/var/www/gamedata/c_images/album1584",
|
||||
"/gamedata/c_images/album1584",
|
||||
0, -1);
|
||||
LOGGER.warn("CustomBadgeManager -> No row found in users_custom_badge_settings, falling back to defaults.");
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("CustomBadgeManager -> Failed to load settings.", e);
|
||||
}
|
||||
|
||||
loadTextCache();
|
||||
}
|
||||
|
||||
public CustomBadgeSettings getSettings() {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
public List<CustomBadge> listForUser(int userId) {
|
||||
List<CustomBadge> result = new ArrayList<>();
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT * FROM `user_custom_badge` WHERE `user_id` = ? ORDER BY `date_created` ASC")) {
|
||||
statement.setInt(1, userId);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
while (resultSet.next()) {
|
||||
result.add(new CustomBadge(resultSet));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("CustomBadgeManager -> Failed to list badges for user " + userId, e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public CustomBadge getByBadgeId(String badgeId) {
|
||||
if (badgeId == null || badgeId.isEmpty()) return null;
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT * FROM `user_custom_badge` WHERE `badge_id` = ? LIMIT 1")) {
|
||||
statement.setString(1, badgeId);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
if (resultSet.next()) {
|
||||
return new CustomBadge(resultSet);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("CustomBadgeManager -> Failed to load badge " + badgeId, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int countForUser(int userId) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT COUNT(*) FROM `user_custom_badge` WHERE `user_id` = ?")) {
|
||||
statement.setInt(1, userId);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
if (resultSet.next()) {
|
||||
return resultSet.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("CustomBadgeManager -> Failed to count badges for user " + userId, e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public CustomBadge create(int userId, String name, String description, byte[] pngBytes) throws CustomBadgeException {
|
||||
enforceRateLimit(userId);
|
||||
|
||||
if (this.countForUser(userId) >= MAX_PER_USER) {
|
||||
throw new CustomBadgeException("limit_reached", "Maximum of " + MAX_PER_USER + " custom badges reached.");
|
||||
}
|
||||
|
||||
BufferedImage image = validatePng(pngBytes);
|
||||
|
||||
chargeForCreate(userId);
|
||||
|
||||
String badgeId = generateBadgeId();
|
||||
int now = (int) (System.currentTimeMillis() / 1000L);
|
||||
|
||||
try {
|
||||
writeBadgeFile(badgeId, image);
|
||||
} catch (CustomBadgeException e) {
|
||||
refundForCreate(userId);
|
||||
throw e;
|
||||
}
|
||||
|
||||
String safeName = sanitize(name, 64);
|
||||
String safeDesc = sanitize(description, 255);
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO `user_custom_badge` (`user_id`, `badge_id`, `badge_name`, `badge_description`, `date_created`, `date_edit`) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
Statement.RETURN_GENERATED_KEYS)) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setString(2, badgeId);
|
||||
statement.setString(3, safeName);
|
||||
statement.setString(4, safeDesc);
|
||||
statement.setInt(5, now);
|
||||
statement.setInt(6, now);
|
||||
statement.executeUpdate();
|
||||
|
||||
int generatedId = 0;
|
||||
try (ResultSet keys = statement.getGeneratedKeys()) {
|
||||
if (keys.next()) generatedId = keys.getInt(1);
|
||||
}
|
||||
|
||||
this.textCache.put(badgeId, new BadgeText(safeName, safeDesc));
|
||||
this.textCacheVersion.incrementAndGet();
|
||||
issueBadgeToInventory(userId, badgeId);
|
||||
|
||||
return new CustomBadge(generatedId, userId, badgeId, safeName, safeDesc, now, now);
|
||||
} catch (SQLException e) {
|
||||
deleteBadgeFileQuietly(badgeId);
|
||||
refundForCreate(userId);
|
||||
LOGGER.error("CustomBadgeManager -> Failed to insert badge for user " + userId, e);
|
||||
throw new CustomBadgeException("db_error", "Could not save the badge.");
|
||||
}
|
||||
}
|
||||
|
||||
public CustomBadge update(int userId, String oldBadgeId, String name, String description, byte[] pngBytes) throws CustomBadgeException {
|
||||
enforceRateLimit(userId);
|
||||
|
||||
CustomBadge existing = getByBadgeId(oldBadgeId);
|
||||
if (existing == null || existing.getUserId() != userId) {
|
||||
throw new CustomBadgeException("not_found", "Badge not found.");
|
||||
}
|
||||
|
||||
BufferedImage image = validatePng(pngBytes);
|
||||
|
||||
String newBadgeId = generateBadgeId();
|
||||
int now = (int) (System.currentTimeMillis() / 1000L);
|
||||
|
||||
writeBadgeFile(newBadgeId, image);
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE `user_custom_badge` SET `badge_id` = ?, `badge_name` = ?, `badge_description` = ?, `date_edit` = ? WHERE `id` = ?")) {
|
||||
statement.setString(1, newBadgeId);
|
||||
statement.setString(2, sanitize(name, 64));
|
||||
statement.setString(3, sanitize(description, 255));
|
||||
statement.setInt(4, now);
|
||||
statement.setInt(5, existing.getId());
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
deleteBadgeFileQuietly(newBadgeId);
|
||||
LOGGER.error("CustomBadgeManager -> Failed to update badge " + oldBadgeId, e);
|
||||
throw new CustomBadgeException("db_error", "Could not update the badge.");
|
||||
}
|
||||
|
||||
String safeName = sanitize(name, 64);
|
||||
String safeDesc = sanitize(description, 255);
|
||||
this.textCache.remove(oldBadgeId);
|
||||
this.textCache.put(newBadgeId, new BadgeText(safeName, safeDesc));
|
||||
this.textCacheVersion.incrementAndGet();
|
||||
renameBadgeInInventory(userId, oldBadgeId, newBadgeId);
|
||||
deleteBadgeFileQuietly(oldBadgeId);
|
||||
return new CustomBadge(existing.getId(), userId, newBadgeId, safeName, safeDesc, existing.getDateCreated(), now);
|
||||
}
|
||||
|
||||
public void delete(int userId, String badgeId) throws CustomBadgeException {
|
||||
enforceRateLimit(userId);
|
||||
|
||||
CustomBadge existing = getByBadgeId(badgeId);
|
||||
if (existing == null || existing.getUserId() != userId) {
|
||||
throw new CustomBadgeException("not_found", "Badge not found.");
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"DELETE FROM `user_custom_badge` WHERE `id` = ?")) {
|
||||
statement.setInt(1, existing.getId());
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("CustomBadgeManager -> Failed to delete badge " + badgeId, e);
|
||||
throw new CustomBadgeException("db_error", "Could not delete the badge.");
|
||||
}
|
||||
|
||||
this.textCache.remove(badgeId);
|
||||
this.textCacheVersion.incrementAndGet();
|
||||
revokeBadgeFromInventory(userId, badgeId);
|
||||
deleteBadgeFileQuietly(badgeId);
|
||||
}
|
||||
|
||||
public boolean isCustomBadgeId(String badgeId) {
|
||||
return badgeId != null && BADGE_ID_PATTERN.matcher(badgeId).matches();
|
||||
}
|
||||
|
||||
public String generateBadgeId() {
|
||||
long timestamp = System.currentTimeMillis() / 1000L;
|
||||
for (int attempt = 0; attempt < 8; attempt++) {
|
||||
StringBuilder suffix = new StringBuilder(RANDOM_SUFFIX_LENGTH);
|
||||
for (int i = 0; i < RANDOM_SUFFIX_LENGTH; i++) {
|
||||
suffix.append(RANDOM_ALPHABET[this.random.nextInt(RANDOM_ALPHABET.length)]);
|
||||
}
|
||||
String candidate = "CUST" + suffix + "-" + timestamp;
|
||||
if (getByBadgeId(candidate) == null) return candidate;
|
||||
timestamp++;
|
||||
}
|
||||
throw new IllegalStateException("Could not allocate a unique custom badge id after 8 attempts.");
|
||||
}
|
||||
|
||||
public String publicUrlFor(String badgeId) {
|
||||
CustomBadgeSettings current = this.settings;
|
||||
if (current == null) return "";
|
||||
String base = current.getBadgeUrl();
|
||||
if (base == null || base.isEmpty()) return "";
|
||||
if (base.endsWith("/")) return base + badgeId + ".gif";
|
||||
return base + "/" + badgeId + ".gif";
|
||||
}
|
||||
|
||||
private void chargeForCreate(int userId) throws CustomBadgeException {
|
||||
CustomBadgeSettings current = this.settings;
|
||||
if (current == null) return;
|
||||
int price = current.getPriceBadge();
|
||||
if (price <= 0) return;
|
||||
|
||||
Habbo habbo = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
|
||||
if (habbo == null) {
|
||||
throw new CustomBadgeException("must_be_online",
|
||||
"You must be online in the hotel to create a paid badge.");
|
||||
}
|
||||
|
||||
int currencyType = current.getCurrencyType();
|
||||
if (currencyType == -1) {
|
||||
if (habbo.getHabboInfo().getCredits() < price) {
|
||||
throw new CustomBadgeException("insufficient_funds",
|
||||
"You don't have enough credits (need " + price + ").");
|
||||
}
|
||||
habbo.giveCredits(-price);
|
||||
} else {
|
||||
if (habbo.getHabboInfo().getCurrencyAmount(currencyType) < price) {
|
||||
throw new CustomBadgeException("insufficient_funds",
|
||||
"You don't have enough of that currency (need " + price + ").");
|
||||
}
|
||||
habbo.givePoints(currencyType, -price);
|
||||
}
|
||||
}
|
||||
|
||||
private void issueBadgeToInventory(int userId, String badgeId) {
|
||||
Habbo online = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
|
||||
if (online != null) {
|
||||
BadgesComponent.createBadge(badgeId, online);
|
||||
if (online.getClient() != null) {
|
||||
online.getClient().sendResponse(new InventoryBadgesComposer(online));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO `users_badges` (`user_id`, `slot_id`, `badge_code`) VALUES (?, 0, ?)")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setString(2, badgeId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("CustomBadgeManager -> Failed to issue offline badge " + badgeId + " to user " + userId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void renameBadgeInInventory(int userId, String oldBadgeId, String newBadgeId) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE `users_badges` SET `badge_code` = ? WHERE `user_id` = ? AND `badge_code` = ?")) {
|
||||
statement.setString(1, newBadgeId);
|
||||
statement.setInt(2, userId);
|
||||
statement.setString(3, oldBadgeId);
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("CustomBadgeManager -> Failed to rename badge in users_badges " + oldBadgeId + " -> " + newBadgeId, e);
|
||||
}
|
||||
|
||||
Habbo online = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
|
||||
if (online == null) return;
|
||||
|
||||
HabboBadge existing = online.getInventory().getBadgesComponent().getBadge(oldBadgeId);
|
||||
if (existing != null) existing.setCode(newBadgeId);
|
||||
|
||||
if (online.getClient() != null) {
|
||||
online.getClient().sendResponse(new InventoryBadgesComposer(online));
|
||||
}
|
||||
}
|
||||
|
||||
private void revokeBadgeFromInventory(int userId, String badgeId) {
|
||||
BadgesComponent.deleteBadge(userId, badgeId);
|
||||
|
||||
Habbo online = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
|
||||
if (online == null) return;
|
||||
|
||||
online.getInventory().getBadgesComponent().removeBadge(badgeId);
|
||||
if (online.getClient() != null) {
|
||||
online.getClient().sendResponse(new InventoryBadgesComposer(online));
|
||||
}
|
||||
}
|
||||
|
||||
private BufferedImage validatePng(byte[] data) throws CustomBadgeException {
|
||||
if (data == null || data.length == 0) {
|
||||
throw new CustomBadgeException("empty", "Badge image is empty.");
|
||||
}
|
||||
if (data.length > MAX_BADGE_SIZE_BYTES) {
|
||||
throw new CustomBadgeException("too_large", "Badge image exceeds " + MAX_BADGE_SIZE_BYTES + " bytes.");
|
||||
}
|
||||
|
||||
if (data.length < PNG_MAGIC.length) {
|
||||
throw new CustomBadgeException("invalid_image", "Badge image must be a PNG.");
|
||||
}
|
||||
for (int i = 0; i < PNG_MAGIC.length; i++) {
|
||||
if (data[i] != PNG_MAGIC[i]) {
|
||||
throw new CustomBadgeException("invalid_image", "Badge image must be a PNG.");
|
||||
}
|
||||
}
|
||||
|
||||
try (ImageInputStream peek = ImageIO.createImageInputStream(new ByteArrayInputStream(data))) {
|
||||
if (peek == null) throw new IOException("no input stream");
|
||||
Iterator<ImageReader> readers = ImageIO.getImageReaders(peek);
|
||||
if (!readers.hasNext()) {
|
||||
throw new CustomBadgeException("invalid_image", "Badge image format not recognised.");
|
||||
}
|
||||
ImageReader reader = readers.next();
|
||||
try {
|
||||
reader.setInput(peek, true, true);
|
||||
int w = reader.getWidth(0);
|
||||
int h = reader.getHeight(0);
|
||||
if (w != BADGE_WIDTH || h != BADGE_HEIGHT) {
|
||||
throw new CustomBadgeException("wrong_dimensions",
|
||||
"Badge image must be " + BADGE_WIDTH + "x" + BADGE_HEIGHT + " pixels.");
|
||||
}
|
||||
} finally {
|
||||
reader.dispose();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new CustomBadgeException("invalid_image", "Badge image header could not be read.");
|
||||
}
|
||||
|
||||
BufferedImage image;
|
||||
try {
|
||||
image = ImageIO.read(new ByteArrayInputStream(data));
|
||||
} catch (IOException e) {
|
||||
throw new CustomBadgeException("invalid_image", "Badge image could not be decoded.");
|
||||
}
|
||||
if (image == null
|
||||
|| image.getWidth() != BADGE_WIDTH
|
||||
|| image.getHeight() != BADGE_HEIGHT) {
|
||||
throw new CustomBadgeException("invalid_image", "Badge image could not be decoded.");
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private void enforceRateLimit(int userId) throws CustomBadgeException {
|
||||
long now = System.currentTimeMillis();
|
||||
long[] bucket = this.rateBuckets.computeIfAbsent(userId, id -> new long[RATE_LIMIT_OPS]);
|
||||
synchronized (bucket) {
|
||||
long oldest = Long.MAX_VALUE;
|
||||
int oldestIdx = 0;
|
||||
for (int i = 0; i < bucket.length; i++) {
|
||||
if (bucket[i] < oldest) { oldest = bucket[i]; oldestIdx = i; }
|
||||
}
|
||||
if (oldest > now - RATE_LIMIT_WINDOW_MS) {
|
||||
throw new CustomBadgeException("rate_limited",
|
||||
"Too many badge operations. Try again in a moment.");
|
||||
}
|
||||
bucket[oldestIdx] = now;
|
||||
}
|
||||
}
|
||||
|
||||
private void refundForCreate(int userId) {
|
||||
CustomBadgeSettings current = this.settings;
|
||||
if (current == null) return;
|
||||
int price = current.getPriceBadge();
|
||||
if (price <= 0) return;
|
||||
|
||||
Habbo habbo = Emulator.getGameServer().getGameClientManager().getHabbo(userId);
|
||||
if (habbo == null) {
|
||||
LOGGER.warn("CustomBadgeManager -> Could not refund {} (price {}): user offline", userId, price);
|
||||
return;
|
||||
}
|
||||
int currencyType = current.getCurrencyType();
|
||||
if (currencyType == -1) habbo.giveCredits(price);
|
||||
else habbo.givePoints(currencyType, price);
|
||||
}
|
||||
|
||||
private void writeBadgeFile(String badgeId, BufferedImage source) throws CustomBadgeException {
|
||||
CustomBadgeSettings current = this.settings;
|
||||
if (current == null || current.getBadgePath() == null || current.getBadgePath().isEmpty()) {
|
||||
throw new CustomBadgeException("not_configured", "Custom badge storage path is not configured.");
|
||||
}
|
||||
try {
|
||||
Path dir = Paths.get(current.getBadgePath()).toAbsolutePath();
|
||||
Files.createDirectories(dir);
|
||||
Path target = dir.resolve(badgeId + ".gif");
|
||||
|
||||
BufferedImage indexed = toIndexedGifImage(source);
|
||||
if (!ImageIO.write(indexed, "gif", target.toFile())) {
|
||||
throw new IOException("No GIF ImageWriter available.");
|
||||
}
|
||||
|
||||
LOGGER.info("CustomBadgeManager -> wrote badge {} ({} bytes) to {}",
|
||||
badgeId, Files.size(target), target);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("CustomBadgeManager -> Failed to write badge " + badgeId
|
||||
+ " to " + current.getBadgePath(), e);
|
||||
throw new CustomBadgeException("write_failed", "Could not save the badge file.");
|
||||
}
|
||||
}
|
||||
|
||||
private static BufferedImage toIndexedGifImage(BufferedImage source) {
|
||||
int w = source.getWidth();
|
||||
int h = source.getHeight();
|
||||
int[] pixels = source.getRGB(0, 0, w, h, null, 0, w);
|
||||
|
||||
Map<Integer, Integer> indexByColor = new LinkedHashMap<>();
|
||||
indexByColor.put(0, 0);
|
||||
|
||||
for (int p : pixels) {
|
||||
int alpha = (p >>> 24) & 0xff;
|
||||
int key = (alpha < 128) ? 0 : (p | 0xFF000000);
|
||||
if (key == 0) continue;
|
||||
if (indexByColor.size() >= 256) break;
|
||||
indexByColor.computeIfAbsent(key, k -> indexByColor.size());
|
||||
}
|
||||
|
||||
int n = indexByColor.size();
|
||||
byte[] r = new byte[n];
|
||||
byte[] g = new byte[n];
|
||||
byte[] b = new byte[n];
|
||||
int i = 0;
|
||||
for (Integer color : indexByColor.keySet()) {
|
||||
r[i] = (byte) ((color >>> 16) & 0xff);
|
||||
g[i] = (byte) ((color >>> 8) & 0xff);
|
||||
b[i] = (byte) (color & 0xff);
|
||||
i++;
|
||||
}
|
||||
|
||||
IndexColorModel colorModel = new IndexColorModel(8, n, r, g, b, 0);
|
||||
BufferedImage out = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_INDEXED, colorModel);
|
||||
|
||||
for (int y = 0; y < h; y++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
int p = pixels[y * w + x];
|
||||
int alpha = (p >>> 24) & 0xff;
|
||||
int key = (alpha < 128) ? 0 : (p | 0xFF000000);
|
||||
Integer idx = indexByColor.get(key);
|
||||
out.getRaster().setSample(x, y, 0, idx == null ? 0 : idx);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private void deleteBadgeFileQuietly(String badgeId) {
|
||||
CustomBadgeSettings current = this.settings;
|
||||
if (current == null || current.getBadgePath() == null) return;
|
||||
File file = new File(current.getBadgePath(), badgeId + ".gif");
|
||||
if (file.exists() && !file.delete()) {
|
||||
LOGGER.warn("CustomBadgeManager -> Could not delete stale badge file: {}", file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
private static String sanitize(String value, int maxLength) {
|
||||
if (value == null) return "";
|
||||
StringBuilder out = new StringBuilder(Math.min(value.length(), maxLength));
|
||||
for (int i = 0; i < value.length() && out.length() < maxLength; i++) {
|
||||
char c = value.charAt(i);
|
||||
if (c < 0x20 || c == 0x7F) continue;
|
||||
out.append(c);
|
||||
}
|
||||
return out.toString().trim();
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.eu.habbo.habbohotel.users.custombadge;
|
||||
|
||||
public class CustomBadgeSettings {
|
||||
|
||||
private final String badgePath;
|
||||
private final String badgeUrl;
|
||||
private final int priceBadge;
|
||||
private final int currencyType;
|
||||
|
||||
public CustomBadgeSettings(String badgePath, String badgeUrl, int priceBadge, int currencyType) {
|
||||
this.badgePath = badgePath;
|
||||
this.badgeUrl = badgeUrl;
|
||||
this.priceBadge = priceBadge;
|
||||
this.currencyType = currencyType;
|
||||
}
|
||||
|
||||
public String getBadgePath() {
|
||||
return this.badgePath;
|
||||
}
|
||||
|
||||
public String getBadgeUrl() {
|
||||
return this.badgeUrl;
|
||||
}
|
||||
|
||||
public int getPriceBadge() {
|
||||
return this.priceBadge;
|
||||
}
|
||||
|
||||
public int getCurrencyType() {
|
||||
return this.currencyType;
|
||||
}
|
||||
}
|
||||
+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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-2
@@ -1,14 +1,30 @@
|
||||
package com.eu.habbo.messages.incoming.users;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
|
||||
public class ActivateEffectEvent extends MessageHandler {
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
int effectId = this.packet.readInt();
|
||||
Habbo habbo = this.client.getHabbo();
|
||||
if (habbo == null) return;
|
||||
|
||||
if (this.client.getHabbo().getInventory().getEffectsComponent().ownsEffect(effectId)) {
|
||||
this.client.getHabbo().getInventory().getEffectsComponent().activateEffect(effectId);
|
||||
if (habbo.getInventory().getEffectsComponent().ownsEffect(effectId)) {
|
||||
habbo.getInventory().getEffectsComponent().activateEffect(effectId);
|
||||
return;
|
||||
}
|
||||
|
||||
int rankId = habbo.getHabboInfo().getRank().getId();
|
||||
if (Emulator.getGameEnvironment().getPermissionsManager().isEffectBlocked(effectId, rankId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = habbo.getHabboInfo().getCurrentRoom();
|
||||
if (room == null || habbo.getHabboInfo().getRiding() != null) return;
|
||||
|
||||
room.giveEffect(habbo, effectId, -1);
|
||||
}
|
||||
}
|
||||
+64
-11
@@ -1,24 +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();
|
||||
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().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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ public class RoomPetComposer extends MessageComposer implements TIntObjectProced
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
if (pet instanceof IPetLook) {
|
||||
this.response.appendString(((IPetLook) pet).getLook());
|
||||
} else {
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ public class RoomUserDataComposer extends MessageComposer {
|
||||
this.response.appendInt(this.habbo.getHabboInfo().getInfostandBg());
|
||||
this.response.appendInt(this.habbo.getHabboInfo().getInfostandStand());
|
||||
this.response.appendInt(this.habbo.getHabboInfo().getInfostandOverlay());
|
||||
this.response.appendInt(this.habbo.getHabboInfo().getInfostandCardBg());
|
||||
return this.response;
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -43,6 +43,7 @@ public class RoomUsersComposer extends MessageComposer {
|
||||
this.response.appendInt(this.habbo.getHabboInfo().getInfostandBg());
|
||||
this.response.appendInt(this.habbo.getHabboInfo().getInfostandStand());
|
||||
this.response.appendInt(this.habbo.getHabboInfo().getInfostandOverlay());
|
||||
this.response.appendInt(this.habbo.getHabboInfo().getInfostandCardBg());
|
||||
this.response.appendString(this.habbo.getHabboInfo().getLook());
|
||||
this.response.appendInt(this.habbo.getRoomUnit().getId()); //Room Unit ID
|
||||
this.response.appendInt(this.habbo.getRoomUnit().getX());
|
||||
@@ -78,6 +79,7 @@ public class RoomUsersComposer extends MessageComposer {
|
||||
this.response.appendInt(habbo.getHabboInfo().getInfostandBg());
|
||||
this.response.appendInt(habbo.getHabboInfo().getInfostandStand());
|
||||
this.response.appendInt(habbo.getHabboInfo().getInfostandOverlay());
|
||||
this.response.appendInt(habbo.getHabboInfo().getInfostandCardBg());
|
||||
this.response.appendString(habbo.getHabboInfo().getLook());
|
||||
this.response.appendInt(habbo.getRoomUnit().getId()); //Room Unit ID
|
||||
this.response.appendInt(habbo.getRoomUnit().getX());
|
||||
@@ -111,6 +113,7 @@ public class RoomUsersComposer extends MessageComposer {
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendString(this.bot.getFigure());
|
||||
this.response.appendInt(this.bot.getRoomUnit().getId());
|
||||
this.response.appendInt(this.bot.getRoomUnit().getX());
|
||||
@@ -143,6 +146,7 @@ public class RoomUsersComposer extends MessageComposer {
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendInt(0);
|
||||
this.response.appendString(bot.getFigure());
|
||||
this.response.appendInt(bot.getRoomUnit().getId());
|
||||
this.response.appendInt(bot.getRoomUnit().getX());
|
||||
|
||||
@@ -115,6 +115,7 @@ public class UserProfileComposer extends MessageComposer {
|
||||
this.response.appendInt(this.habboInfo.getInfostandBg());
|
||||
this.response.appendInt(this.habboInfo.getInfostandStand());
|
||||
this.response.appendInt(this.habboInfo.getInfostandOverlay());
|
||||
this.response.appendInt(this.habboInfo.getInfostandCardBg());
|
||||
|
||||
return this.response;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,16 @@ public class GameServer extends Server {
|
||||
LOGGER.error("Failed to start WebSocket server on {}:{}", wsHost, wsPort);
|
||||
} else {
|
||||
LOGGER.info("WebSocket server started on {}:{} (SSL: {})", wsHost, wsPort, wsInitializer.isSslEnabled());
|
||||
|
||||
if (com.eu.habbo.Emulator.getConfig().getBoolean("crypto.ws.signing.enabled", false)) {
|
||||
try {
|
||||
com.eu.habbo.networking.gameserver.crypto.CryptoSigningKeyManager.get();
|
||||
LOGGER.info("[ws-crypto] signing public key ready: {}",
|
||||
com.eu.habbo.networking.gameserver.crypto.CryptoSigningKeyManager.publicKeyBase64());
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[ws-crypto] failed to warm signing keypair", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,4 +10,6 @@ public class GameServerAttributes {
|
||||
public static final AttributeKey<HabboRC4> CRYPTO_CLIENT = AttributeKey.valueOf("CryptoClient");
|
||||
public static final AttributeKey<HabboRC4> CRYPTO_SERVER = AttributeKey.valueOf("CryptoServer");
|
||||
public static final AttributeKey<String> WS_IP = AttributeKey.valueOf("WebSocketIP");
|
||||
public static final AttributeKey<byte[]> WS_AES_KEY = AttributeKey.valueOf("WsAesKey");
|
||||
}
|
||||
|
||||
|
||||
+10
-3
@@ -1,7 +1,11 @@
|
||||
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.badges.BadgeHttpHandler;
|
||||
import com.eu.habbo.networking.gameserver.codec.WebSocketCodec;
|
||||
import com.eu.habbo.networking.gameserver.crypto.WsHandshakeHandler;
|
||||
import com.eu.habbo.networking.gameserver.decoders.*;
|
||||
import com.eu.habbo.networking.gameserver.encoders.GameServerMessageEncoder;
|
||||
import com.eu.habbo.networking.gameserver.encoders.GameServerMessageLogger;
|
||||
@@ -49,10 +53,15 @@ 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("authHttpHandler", new AuthHttpHandler());
|
||||
ch.pipeline().addLast("badgeHttpHandler", new BadgeHttpHandler());
|
||||
ch.pipeline().addLast("wsProtocolHandler", new WebSocketServerProtocolHandler(this.wsConfig));
|
||||
ch.pipeline().addLast("wsCodec", new WebSocketCodec());
|
||||
|
||||
// Standard game decoders
|
||||
if (Emulator.getConfig().getBoolean("crypto.ws.enabled", false)) {
|
||||
ch.pipeline().addLast(WsHandshakeHandler.HANDLER_NAME, new WsHandshakeHandler());
|
||||
}
|
||||
|
||||
ch.pipeline().addLast(new GamePolicyDecoder());
|
||||
ch.pipeline().addLast(new GameByteFrameDecoder());
|
||||
ch.pipeline().addLast(new GameByteDecoder());
|
||||
@@ -64,8 +73,6 @@ public class WebSocketChannelInitializer extends ChannelInitializer<SocketChanne
|
||||
ch.pipeline().addLast("idleEventHandler", new IdleTimeoutHandler(30, 60));
|
||||
ch.pipeline().addLast(new GameMessageRateLimit());
|
||||
ch.pipeline().addLast(new GameMessageHandler());
|
||||
|
||||
// Encoders
|
||||
ch.pipeline().addLast("messageEncoder", new GameServerMessageEncoder());
|
||||
|
||||
if (PacketManager.DEBUG_SHOW_PACKETS) {
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package com.eu.habbo.networking.gameserver.auth;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Base64;
|
||||
|
||||
public final class AccessTokenService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AccessTokenService.class);
|
||||
private static final SecureRandom RNG = new SecureRandom();
|
||||
private static final Base64.Encoder URL_ENC = Base64.getUrlEncoder().withoutPadding();
|
||||
private static final Base64.Decoder URL_DEC = Base64.getUrlDecoder();
|
||||
|
||||
private static volatile String cachedSecret = null;
|
||||
|
||||
private AccessTokenService() {}
|
||||
|
||||
public static final class Issued {
|
||||
public final String token;
|
||||
public final long expiresAt;
|
||||
|
||||
Issued(String token, long expiresAt) {
|
||||
this.token = token;
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
}
|
||||
|
||||
public static long ttlSeconds() {
|
||||
return Math.max(60L, Emulator.getConfig().getInt("login.access.jwt.ttl.seconds", 86400));
|
||||
}
|
||||
|
||||
public static Issued issue(int userId) {
|
||||
long now = Emulator.getIntUnixTimestamp();
|
||||
long exp = now + ttlSeconds();
|
||||
|
||||
JsonObject header = new JsonObject();
|
||||
header.addProperty("alg", "HS256");
|
||||
header.addProperty("typ", "JWT");
|
||||
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("sub", userId);
|
||||
payload.addProperty("iat", now);
|
||||
payload.addProperty("exp", exp);
|
||||
payload.addProperty("typ", "access");
|
||||
|
||||
String h = URL_ENC.encodeToString(header.toString().getBytes(StandardCharsets.UTF_8));
|
||||
String p = URL_ENC.encodeToString(payload.toString().getBytes(StandardCharsets.UTF_8));
|
||||
String signingInput = h + "." + p;
|
||||
String sig = URL_ENC.encodeToString(hmacSha256(secret().getBytes(StandardCharsets.UTF_8),
|
||||
signingInput.getBytes(StandardCharsets.UTF_8)));
|
||||
return new Issued(signingInput + "." + sig, exp);
|
||||
}
|
||||
|
||||
public static int verify(String token) {
|
||||
if (token == null || token.isEmpty()) return 0;
|
||||
|
||||
String[] parts = token.split("\\.");
|
||||
if (parts.length != 3) return 0;
|
||||
|
||||
try {
|
||||
String signingInput = parts[0] + "." + parts[1];
|
||||
byte[] expected = hmacSha256(secret().getBytes(StandardCharsets.UTF_8),
|
||||
signingInput.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] provided = URL_DEC.decode(parts[2]);
|
||||
if (!constantTimeEquals(expected, provided)) return 0;
|
||||
|
||||
byte[] payloadBytes = URL_DEC.decode(parts[1]);
|
||||
JsonObject payload = JsonParser.parseString(new String(payloadBytes, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
|
||||
if (!payload.has("typ") || !"access".equals(payload.get("typ").getAsString())) return 0;
|
||||
long exp = payload.get("exp").getAsLong();
|
||||
if (exp <= Emulator.getIntUnixTimestamp()) return 0;
|
||||
return payload.get("sub").getAsInt();
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static String secret() {
|
||||
String s = cachedSecret;
|
||||
if (s != null && !s.isEmpty()) return s;
|
||||
|
||||
synchronized (AccessTokenService.class) {
|
||||
if (cachedSecret != null && !cachedSecret.isEmpty()) return cachedSecret;
|
||||
|
||||
String configured = Emulator.getConfig().getValue("login.access.jwt.secret", "");
|
||||
if (configured != null && !configured.isEmpty()) {
|
||||
cachedSecret = configured;
|
||||
return configured;
|
||||
}
|
||||
|
||||
byte[] buf = new byte[48];
|
||||
RNG.nextBytes(buf);
|
||||
String generated = Base64.getEncoder().withoutPadding().encodeToString(buf);
|
||||
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(
|
||||
"INSERT INTO emulator_settings (`key`, `value`) VALUES ('login.access.jwt.secret', ?) "
|
||||
+ "ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)")) {
|
||||
stmt.setString(1, generated);
|
||||
stmt.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Could not persist generated login.access.jwt.secret; using in-memory only", e);
|
||||
}
|
||||
|
||||
Emulator.getConfig().update("login.access.jwt.secret", generated);
|
||||
cachedSecret = generated;
|
||||
LOGGER.info("[auth/access] generated new access token signing secret (persisted to emulator_settings)");
|
||||
return generated;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] hmacSha256(byte[] key, byte[] data) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(key, "HmacSHA256"));
|
||||
return mac.doFinal(data);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("HmacSHA256 unavailable", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean constantTimeEquals(byte[] a, byte[] b) {
|
||||
if (a == null || b == null || a.length != b.length) return false;
|
||||
int r = 0;
|
||||
for (int i = 0; i < a.length; i++) r |= a[i] ^ b[i];
|
||||
return r == 0;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,102 @@
|
||||
package com.eu.habbo.networking.gameserver.auth;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class AuthRateLimiter {
|
||||
|
||||
private static final Map<String, AtomicReference<State>> STATE = new ConcurrentHashMap<>();
|
||||
private static final Map<String, AtomicReference<ProbeState>> PROBE_STATE = new ConcurrentHashMap<>();
|
||||
|
||||
private AuthRateLimiter() {}
|
||||
|
||||
public static boolean isLocked(String ip) {
|
||||
if (!isEnabled() || ip == null || ip.isEmpty()) return false;
|
||||
|
||||
AtomicReference<State> ref = STATE.get(ip);
|
||||
if (ref == null) return false;
|
||||
|
||||
State current = ref.get();
|
||||
return current != null && current.lockedUntilMillis > System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public static long secondsUntilUnlock(String ip) {
|
||||
AtomicReference<State> ref = STATE.get(ip);
|
||||
if (ref == null) return 0;
|
||||
|
||||
State current = ref.get();
|
||||
if (current == null) return 0;
|
||||
|
||||
long remainingMs = current.lockedUntilMillis - System.currentTimeMillis();
|
||||
return remainingMs > 0 ? (remainingMs / 1000L) + 1L : 0L;
|
||||
}
|
||||
|
||||
public static void recordFailure(String ip) {
|
||||
if (!isEnabled() || ip == null || ip.isEmpty()) return;
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long windowMs = configInt("login.ratelimit.window_sec", 60) * 1000L;
|
||||
int maxAttempts = configInt("login.ratelimit.max_attempts", 5);
|
||||
long lockoutMs = configInt("login.ratelimit.lockout_sec", 120) * 1000L;
|
||||
|
||||
STATE.computeIfAbsent(ip, k -> new AtomicReference<>(new State(0, 0L, 0L)))
|
||||
.updateAndGet(prev -> {
|
||||
if (prev == null || (now - prev.windowStartMillis) > windowMs) {
|
||||
return new State(1, now, 0L);
|
||||
}
|
||||
|
||||
int attempts = prev.attempts + 1;
|
||||
long lockedUntil = attempts >= maxAttempts ? now + lockoutMs : 0L;
|
||||
return new State(attempts, prev.windowStartMillis, lockedUntil);
|
||||
});
|
||||
}
|
||||
|
||||
public static void recordSuccess(String ip) {
|
||||
if (ip == null || ip.isEmpty()) return;
|
||||
STATE.remove(ip);
|
||||
}
|
||||
|
||||
public static boolean tryProbe(String ip) {
|
||||
if (!isEnabled() || ip == null || ip.isEmpty()) return true;
|
||||
if (isLocked(ip)) return false;
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long windowMs = configInt("login.probe.window_sec", 60) * 1000L;
|
||||
int maxAttempts = configInt("login.probe.max_attempts", 20);
|
||||
|
||||
ProbeState next = PROBE_STATE.computeIfAbsent(ip, k -> new AtomicReference<>(new ProbeState(0, now)))
|
||||
.updateAndGet(prev -> {
|
||||
if (prev == null || (now - prev.windowStartMillis) > windowMs) {
|
||||
return new ProbeState(1, now);
|
||||
}
|
||||
return new ProbeState(prev.count + 1, prev.windowStartMillis);
|
||||
});
|
||||
|
||||
return next.count <= maxAttempts;
|
||||
}
|
||||
|
||||
public static long secondsUntilProbeReset(String ip) {
|
||||
AtomicReference<ProbeState> ref = PROBE_STATE.get(ip);
|
||||
if (ref == null) return 0;
|
||||
ProbeState current = ref.get();
|
||||
if (current == null) return 0;
|
||||
long windowMs = configInt("login.probe.window_sec", 60) * 1000L;
|
||||
long remainingMs = (current.windowStartMillis + windowMs) - System.currentTimeMillis();
|
||||
return remainingMs > 0 ? (remainingMs / 1000L) + 1L : 0L;
|
||||
}
|
||||
|
||||
private static boolean isEnabled() {
|
||||
return Emulator.getConfig() != null
|
||||
&& Emulator.getConfig().getBoolean("login.ratelimit.enabled", true);
|
||||
}
|
||||
|
||||
private static int configInt(String key, int fallback) {
|
||||
return Emulator.getConfig() != null ? Emulator.getConfig().getInt(key, fallback) : fallback;
|
||||
}
|
||||
|
||||
private record State(int attempts, long windowStartMillis, long lockedUntilMillis) {}
|
||||
private record ProbeState(int count, long windowStartMillis) {}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.eu.habbo.networking.gameserver.auth;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class AvailabilityCache {
|
||||
|
||||
private static final Map<String, Entry> EMAIL_CACHE = new ConcurrentHashMap<>();
|
||||
private static final Map<String, Entry> USERNAME_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private AvailabilityCache() {}
|
||||
|
||||
public static Boolean lookupEmail(String email) {
|
||||
return read(EMAIL_CACHE, key(email));
|
||||
}
|
||||
|
||||
public static Boolean lookupUsername(String username) {
|
||||
return read(USERNAME_CACHE, key(username));
|
||||
}
|
||||
|
||||
public static void storeEmail(String email, boolean available) {
|
||||
write(EMAIL_CACHE, key(email), available);
|
||||
}
|
||||
|
||||
public static void storeUsername(String username, boolean available) {
|
||||
write(USERNAME_CACHE, key(username), available);
|
||||
}
|
||||
|
||||
public static void invalidateEmail(String email) {
|
||||
EMAIL_CACHE.remove(key(email));
|
||||
}
|
||||
|
||||
public static void invalidateUsername(String username) {
|
||||
USERNAME_CACHE.remove(key(username));
|
||||
}
|
||||
|
||||
private static String key(String value) {
|
||||
return value == null ? "" : value.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static Boolean read(Map<String, Entry> cache, String key) {
|
||||
if (!isEnabled() || key.isEmpty()) return null;
|
||||
Entry entry = cache.get(key);
|
||||
if (entry == null) return null;
|
||||
if (entry.expiresAt < System.currentTimeMillis()) {
|
||||
cache.remove(key, entry);
|
||||
return null;
|
||||
}
|
||||
return entry.available;
|
||||
}
|
||||
|
||||
private static void write(Map<String, Entry> cache, String key, boolean available) {
|
||||
if (!isEnabled() || key.isEmpty()) return;
|
||||
|
||||
int maxEntries = configInt("login.probe.cache_max_entries", 10_000);
|
||||
if (cache.size() >= maxEntries) evict(cache, maxEntries);
|
||||
|
||||
long ttlMs = configInt("login.probe.cache_ttl_sec", 60) * 1000L;
|
||||
cache.put(key, new Entry(available, System.currentTimeMillis() + ttlMs));
|
||||
}
|
||||
|
||||
private static void evict(Map<String, Entry> cache, int maxEntries) {
|
||||
long now = System.currentTimeMillis();
|
||||
cache.values().removeIf(e -> e.expiresAt < now);
|
||||
|
||||
if (cache.size() < maxEntries) return;
|
||||
|
||||
int overflow = cache.size() - maxEntries + 1;
|
||||
Iterator<String> it = cache.keySet().iterator();
|
||||
while (overflow > 0 && it.hasNext()) {
|
||||
it.next();
|
||||
it.remove();
|
||||
overflow--;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isEnabled() {
|
||||
return Emulator.getConfig() == null
|
||||
|| Emulator.getConfig().getBoolean("login.probe.cache_enabled", true);
|
||||
}
|
||||
|
||||
private static int configInt(String key, int fallback) {
|
||||
return Emulator.getConfig() != null ? Emulator.getConfig().getInt(key, fallback) : fallback;
|
||||
}
|
||||
|
||||
private record Entry(boolean available, long expiresAt) {}
|
||||
}
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
package com.eu.habbo.networking.gameserver.auth;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
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.sql.Statement;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class RememberJwtService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RememberJwtService.class);
|
||||
private static final SecureRandom RNG = new SecureRandom();
|
||||
private static final Base64.Encoder URL_ENC = Base64.getUrlEncoder().withoutPadding();
|
||||
private static final Base64.Decoder URL_DEC = Base64.getUrlDecoder();
|
||||
|
||||
private static volatile String cachedSecret = null;
|
||||
|
||||
private RememberJwtService() {}
|
||||
|
||||
public static final class RotationResult {
|
||||
public final String jwt;
|
||||
public final int userId;
|
||||
public final String username;
|
||||
public final long expiresAt;
|
||||
|
||||
RotationResult(String jwt, int userId, String username, long expiresAt) {
|
||||
this.jwt = jwt;
|
||||
this.userId = userId;
|
||||
this.username = username;
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
}
|
||||
|
||||
private static int familyTtlDays() {
|
||||
return Math.max(1, Emulator.getConfig().getInt("login.remember.duration.days", 30));
|
||||
}
|
||||
|
||||
private static long familyTtlSeconds() {
|
||||
return familyTtlDays() * 86400L;
|
||||
}
|
||||
|
||||
private static String secret() {
|
||||
String s = cachedSecret;
|
||||
if (s != null && !s.isEmpty()) return s;
|
||||
|
||||
synchronized (RememberJwtService.class) {
|
||||
if (cachedSecret != null && !cachedSecret.isEmpty()) return cachedSecret;
|
||||
|
||||
String configured = Emulator.getConfig().getValue("login.remember.jwt.secret", "");
|
||||
if (configured != null && !configured.isEmpty()) {
|
||||
cachedSecret = configured;
|
||||
return configured;
|
||||
}
|
||||
|
||||
byte[] buf = new byte[48];
|
||||
RNG.nextBytes(buf);
|
||||
String generated = Base64.getEncoder().withoutPadding().encodeToString(buf);
|
||||
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(
|
||||
"INSERT INTO emulator_settings (`key`, `value`) VALUES ('login.remember.jwt.secret', ?) "
|
||||
+ "ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)")) {
|
||||
stmt.setString(1, generated);
|
||||
stmt.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("Could not persist generated login.remember.jwt.secret; using in-memory only", e);
|
||||
}
|
||||
|
||||
Emulator.getConfig().update("login.remember.jwt.secret", generated);
|
||||
cachedSecret = generated;
|
||||
LOGGER.info("[auth/remember] generated new JWT signing secret (persisted to emulator_settings)");
|
||||
return generated;
|
||||
}
|
||||
}
|
||||
|
||||
public static RotationResult issueForNewFamily(Connection conn, int userId, String username, String ip) throws SQLException {
|
||||
String familyId = UUID.randomUUID().toString();
|
||||
long now = Emulator.getIntUnixTimestamp();
|
||||
long expiresAt = now + familyTtlSeconds();
|
||||
|
||||
try (PreparedStatement ins = conn.prepareStatement(
|
||||
"INSERT INTO users_remember_families (family_id, user_id, current_version, created_at, expires_at, revoked, last_ip) "
|
||||
+ "VALUES (?, ?, 1, ?, ?, 0, ?)")) {
|
||||
ins.setString(1, familyId);
|
||||
ins.setInt(2, userId);
|
||||
ins.setLong(3, now);
|
||||
ins.setLong(4, expiresAt);
|
||||
ins.setString(5, ip == null ? "" : ip);
|
||||
ins.executeUpdate();
|
||||
}
|
||||
|
||||
String jwt = buildJwt(userId, familyId, 1, now, expiresAt);
|
||||
return new RotationResult(jwt, userId, username, expiresAt);
|
||||
}
|
||||
|
||||
public static RotationResult rotate(Connection conn, String jwt, String ip) {
|
||||
ParsedJwt parsed;
|
||||
try {
|
||||
parsed = verifyAndParse(jwt);
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug("[auth/remember] invalid JWT: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
long now = Emulator.getIntUnixTimestamp();
|
||||
if (parsed.exp <= now) return null;
|
||||
|
||||
int familyVersion = 0;
|
||||
boolean revoked = false;
|
||||
long familyExpiresAt = 0;
|
||||
try (PreparedStatement sel = conn.prepareStatement(
|
||||
"SELECT current_version, revoked, expires_at FROM users_remember_families WHERE family_id = ? AND user_id = ? LIMIT 1")) {
|
||||
sel.setString(1, parsed.familyId);
|
||||
sel.setInt(2, parsed.userId);
|
||||
try (ResultSet rs = sel.executeQuery()) {
|
||||
if (!rs.next()) return null;
|
||||
familyVersion = rs.getInt("current_version");
|
||||
revoked = rs.getInt("revoked") != 0;
|
||||
familyExpiresAt = rs.getLong("expires_at");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/remember] family lookup failed", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (revoked || familyExpiresAt <= now) return null;
|
||||
|
||||
if (parsed.version < familyVersion) {
|
||||
LOGGER.warn("[auth/remember] replay detected: familyId={} presented v={} but current is v={}, revoking family",
|
||||
parsed.familyId, parsed.version, familyVersion);
|
||||
revokeFamilyById(conn, parsed.familyId);
|
||||
return null;
|
||||
}
|
||||
if (parsed.version > familyVersion) {
|
||||
LOGGER.warn("[auth/remember] future version: familyId={} presented v={} but current is v={}",
|
||||
parsed.familyId, parsed.version, familyVersion);
|
||||
return null;
|
||||
}
|
||||
|
||||
int newVersion = familyVersion + 1;
|
||||
long newExpiresAt = now + familyTtlSeconds();
|
||||
|
||||
try (PreparedStatement upd = conn.prepareStatement(
|
||||
"UPDATE users_remember_families SET current_version = ?, expires_at = ?, last_ip = ? "
|
||||
+ "WHERE family_id = ? AND current_version = ? AND revoked = 0")) {
|
||||
upd.setInt(1, newVersion);
|
||||
upd.setLong(2, newExpiresAt);
|
||||
upd.setString(3, ip == null ? "" : ip);
|
||||
upd.setString(4, parsed.familyId);
|
||||
upd.setInt(5, familyVersion);
|
||||
int rows = upd.executeUpdate();
|
||||
if (rows == 0) return null;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/remember] rotation update failed", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
String username = null;
|
||||
try (PreparedStatement usr = conn.prepareStatement("SELECT username FROM users WHERE id = ? LIMIT 1")) {
|
||||
usr.setInt(1, parsed.userId);
|
||||
try (ResultSet rs = usr.executeQuery()) {
|
||||
if (rs.next()) username = rs.getString("username");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/remember] username lookup failed", e);
|
||||
}
|
||||
|
||||
if (username == null) return null;
|
||||
|
||||
String newJwt = buildJwt(parsed.userId, parsed.familyId, newVersion, now, newExpiresAt);
|
||||
return new RotationResult(newJwt, parsed.userId, username, newExpiresAt);
|
||||
}
|
||||
|
||||
public static void revokeFromToken(Connection conn, String jwt) {
|
||||
try {
|
||||
ParsedJwt p = verifyAndParse(jwt);
|
||||
revokeFamilyById(conn, p.familyId);
|
||||
} catch (Exception ignored) { }
|
||||
}
|
||||
|
||||
private static void revokeFamilyById(Connection conn, String familyId) {
|
||||
try (PreparedStatement upd = conn.prepareStatement(
|
||||
"UPDATE users_remember_families SET revoked = 1 WHERE family_id = ?")) {
|
||||
upd.setString(1, familyId);
|
||||
upd.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LOGGER.error("[auth/remember] revoke failed for familyId=" + familyId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String buildJwt(int userId, String familyId, int version, long iat, long exp) {
|
||||
JsonObject header = new JsonObject();
|
||||
header.addProperty("alg", "HS256");
|
||||
header.addProperty("typ", "JWT");
|
||||
|
||||
JsonObject payload = new JsonObject();
|
||||
payload.addProperty("sub", userId);
|
||||
payload.addProperty("fid", familyId);
|
||||
payload.addProperty("v", version);
|
||||
payload.addProperty("iat", iat);
|
||||
payload.addProperty("exp", exp);
|
||||
payload.addProperty("typ", "refresh");
|
||||
|
||||
String h = URL_ENC.encodeToString(header.toString().getBytes(StandardCharsets.UTF_8));
|
||||
String p = URL_ENC.encodeToString(payload.toString().getBytes(StandardCharsets.UTF_8));
|
||||
String signingInput = h + "." + p;
|
||||
String sig = URL_ENC.encodeToString(hmacSha256(secret().getBytes(StandardCharsets.UTF_8),
|
||||
signingInput.getBytes(StandardCharsets.UTF_8)));
|
||||
return signingInput + "." + sig;
|
||||
}
|
||||
|
||||
private static final class ParsedJwt {
|
||||
final int userId;
|
||||
final String familyId;
|
||||
final int version;
|
||||
final long exp;
|
||||
|
||||
ParsedJwt(int userId, String familyId, int version, long exp) {
|
||||
this.userId = userId;
|
||||
this.familyId = familyId;
|
||||
this.version = version;
|
||||
this.exp = exp;
|
||||
}
|
||||
}
|
||||
|
||||
private static ParsedJwt verifyAndParse(String jwt) throws Exception {
|
||||
if (jwt == null || jwt.isEmpty()) throw new IllegalArgumentException("empty");
|
||||
|
||||
String[] parts = jwt.split("\\.");
|
||||
if (parts.length != 3) throw new IllegalArgumentException("not 3 segments");
|
||||
|
||||
String signingInput = parts[0] + "." + parts[1];
|
||||
byte[] expected = hmacSha256(secret().getBytes(StandardCharsets.UTF_8), signingInput.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] provided = URL_DEC.decode(parts[2]);
|
||||
if (!constantTimeEquals(expected, provided)) throw new SecurityException("bad signature");
|
||||
|
||||
byte[] payloadBytes = URL_DEC.decode(parts[1]);
|
||||
JsonObject payload = JsonParser.parseString(new String(payloadBytes, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
|
||||
if (!payload.has("typ") || !"refresh".equals(payload.get("typ").getAsString())) throw new IllegalArgumentException("wrong typ");
|
||||
int userId = payload.get("sub").getAsInt();
|
||||
String fid = payload.get("fid").getAsString();
|
||||
int version = payload.get("v").getAsInt();
|
||||
long exp = payload.get("exp").getAsLong();
|
||||
|
||||
return new ParsedJwt(userId, fid, version, exp);
|
||||
}
|
||||
|
||||
private static byte[] hmacSha256(byte[] key, byte[] data) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(key, "HmacSHA256"));
|
||||
return mac.doFinal(data);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("HmacSHA256 unavailable", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean constantTimeEquals(byte[] a, byte[] b) {
|
||||
if (a == null || b == null || a.length != b.length) return false;
|
||||
int r = 0;
|
||||
for (int i = 0; i < a.length; i++) r |= a[i] ^ b[i];
|
||||
return r == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.eu.habbo.networking.gameserver.auth;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import jakarta.mail.Authenticator;
|
||||
import jakarta.mail.Message;
|
||||
import jakarta.mail.PasswordAuthentication;
|
||||
import jakarta.mail.Session;
|
||||
import jakarta.mail.Transport;
|
||||
import jakarta.mail.internet.InternetAddress;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
public final class SmtpMailService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SmtpMailService.class);
|
||||
|
||||
private SmtpMailService() {}
|
||||
|
||||
public static boolean send(String toAddress, String subject, String body) {
|
||||
try {
|
||||
String provider = Emulator.getConfig().getValue("smtp.provider", "own").toLowerCase();
|
||||
String username = Emulator.getConfig().getValue("smtp.username", "");
|
||||
String password = Emulator.getConfig().getValue("smtp.password", "");
|
||||
String fromAddr = Emulator.getConfig().getValue("smtp.from_address", username);
|
||||
String fromName = Emulator.getConfig().getValue("smtp.from_name", "Habbo Hotel");
|
||||
|
||||
if (toAddress == null || toAddress.isEmpty() || fromAddr == null || fromAddr.isEmpty()) {
|
||||
LOGGER.warn("SMTP send aborted — missing to/from address (to={}, from={})", toAddress, fromAddr);
|
||||
return false;
|
||||
}
|
||||
|
||||
String host;
|
||||
int port;
|
||||
boolean useSsl;
|
||||
boolean useTls;
|
||||
|
||||
switch (provider) {
|
||||
case "gmail" -> {
|
||||
host = "smtp.gmail.com";
|
||||
port = 465;
|
||||
useSsl = true;
|
||||
useTls = false;
|
||||
}
|
||||
case "sendgrid" -> {
|
||||
host = "smtp.sendgrid.net";
|
||||
port = 587;
|
||||
useSsl = false;
|
||||
useTls = true;
|
||||
}
|
||||
case "mailgun" -> {
|
||||
host = "smtp.mailgun.org";
|
||||
port = 587;
|
||||
useSsl = false;
|
||||
useTls = true;
|
||||
}
|
||||
default -> {
|
||||
host = Emulator.getConfig().getValue("smtp.host", "localhost");
|
||||
port = Emulator.getConfig().getInt("smtp.port", 587);
|
||||
useSsl = Emulator.getConfig().getBoolean("smtp.use_ssl", false);
|
||||
useTls = Emulator.getConfig().getBoolean("smtp.use_tls", true);
|
||||
}
|
||||
}
|
||||
|
||||
Properties props = new Properties();
|
||||
props.put("mail.smtp.host", host);
|
||||
props.put("mail.smtp.port", String.valueOf(port));
|
||||
props.put("mail.smtp.auth", String.valueOf(!username.isEmpty()));
|
||||
if (useTls) props.put("mail.smtp.starttls.enable", "true");
|
||||
if (useSsl) {
|
||||
props.put("mail.smtp.ssl.enable", "true");
|
||||
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
|
||||
props.put("mail.smtp.socketFactory.port", String.valueOf(port));
|
||||
}
|
||||
props.put("mail.smtp.connectiontimeout", "10000");
|
||||
props.put("mail.smtp.timeout", "10000");
|
||||
|
||||
Session session = username.isEmpty()
|
||||
? Session.getInstance(props)
|
||||
: Session.getInstance(props, new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
return new PasswordAuthentication(username, password);
|
||||
}
|
||||
});
|
||||
|
||||
MimeMessage message = new MimeMessage(session);
|
||||
message.setFrom(new InternetAddress(fromAddr, fromName, "UTF-8"));
|
||||
message.addRecipient(Message.RecipientType.TO, new InternetAddress(toAddress));
|
||||
message.setSubject(subject, "UTF-8");
|
||||
message.setText(body, "UTF-8");
|
||||
|
||||
Transport.send(message);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to send SMTP mail to " + toAddress, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.eu.habbo.networking.gameserver.auth;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
|
||||
public final class TurnstileVerifier {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(TurnstileVerifier.class);
|
||||
private static final String VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
|
||||
|
||||
private static final HttpClient CLIENT = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(5))
|
||||
.build();
|
||||
|
||||
private TurnstileVerifier() {}
|
||||
|
||||
public static boolean isEnabled() {
|
||||
return Emulator.getConfig() != null
|
||||
&& Emulator.getConfig().getBoolean("login.turnstile.enabled", false);
|
||||
}
|
||||
|
||||
public static boolean verify(String token, String remoteIp) {
|
||||
if (!isEnabled()) return true;
|
||||
|
||||
if (token == null || token.isEmpty()) return false;
|
||||
|
||||
String secret = Emulator.getConfig().getValue("login.turnstile.secretkey", "");
|
||||
if (secret.isEmpty()) {
|
||||
LOGGER.warn("login.turnstile.enabled=1 but login.turnstile.secretkey is empty — refusing the request");
|
||||
return false;
|
||||
}
|
||||
|
||||
StringBuilder form = new StringBuilder();
|
||||
form.append("secret=").append(URLEncoder.encode(secret, StandardCharsets.UTF_8));
|
||||
form.append("&response=").append(URLEncoder.encode(token, StandardCharsets.UTF_8));
|
||||
if (remoteIp != null && !remoteIp.isEmpty()) {
|
||||
form.append("&remoteip=").append(URLEncoder.encode(remoteIp, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(VERIFY_URL))
|
||||
.timeout(Duration.ofSeconds(8))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(form.toString(), StandardCharsets.UTF_8))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
LOGGER.warn("Turnstile siteverify returned HTTP {} for ip={}", response.statusCode(), remoteIp);
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
|
||||
boolean success = json.has("success") && json.get("success").getAsBoolean();
|
||||
if (!success) {
|
||||
LOGGER.info("Turnstile token rejected for ip={} body={}", remoteIp, response.body());
|
||||
}
|
||||
return success;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Turnstile verification failed for ip=" + remoteIp, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+371
@@ -0,0 +1,371 @@
|
||||
package com.eu.habbo.networking.gameserver.badges;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.users.custombadge.CustomBadge;
|
||||
import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeException;
|
||||
import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeManager;
|
||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||
import com.eu.habbo.networking.gameserver.auth.AccessTokenService;
|
||||
import com.eu.habbo.networking.gameserver.auth.AuthRateLimiter;
|
||||
import com.google.gson.JsonArray;
|
||||
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 java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
public class BadgeHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(BadgeHttpHandler.class);
|
||||
|
||||
private static final String BASE_PATH = "/api/badges/custom";
|
||||
private static final int MAX_BODY_BYTES = 128 * 1024;
|
||||
|
||||
private static volatile JsonObject cachedTextsResponse = null;
|
||||
private static volatile long cachedTextsVersion = -1L;
|
||||
|
||||
@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 (!path.equals(BASE_PATH) && !path.startsWith(BASE_PATH + "/")) {
|
||||
super.channelRead(ctx, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
handle(ctx, req, path);
|
||||
} finally {
|
||||
ReferenceCountUtil.release(req);
|
||||
}
|
||||
}
|
||||
|
||||
private void handle(ChannelHandlerContext ctx, FullHttpRequest req, String path) {
|
||||
if (req.method() == HttpMethod.OPTIONS) {
|
||||
sendCors(ctx, req);
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.equals(BASE_PATH + "/texts")) {
|
||||
if (req.method() == HttpMethod.GET || req.method() == HttpMethod.HEAD) {
|
||||
String ip = resolveClientIp(ctx, req);
|
||||
if (!AuthRateLimiter.tryProbe(ip)) {
|
||||
long secs = AuthRateLimiter.secondsUntilProbeReset(ip);
|
||||
sendJson(ctx, req, HttpResponseStatus.TOO_MANY_REQUESTS,
|
||||
error("Too many requests. Try again in " + secs + "s."));
|
||||
return;
|
||||
}
|
||||
handleTexts(ctx, req);
|
||||
return;
|
||||
}
|
||||
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, error("Use GET."));
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = authenticate(req);
|
||||
if (userId == 0) {
|
||||
sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, error("Authentication required."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.content().readableBytes() > MAX_BODY_BYTES) {
|
||||
sendJson(ctx, req, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, error("Payload too large."));
|
||||
return;
|
||||
}
|
||||
|
||||
String trailing = path.length() > BASE_PATH.length() ? path.substring(BASE_PATH.length() + 1) : "";
|
||||
|
||||
try {
|
||||
if (trailing.isEmpty()) {
|
||||
if (req.method() == HttpMethod.GET || req.method() == HttpMethod.HEAD) {
|
||||
handleList(ctx, req, userId);
|
||||
return;
|
||||
}
|
||||
if (req.method() == HttpMethod.POST) {
|
||||
handleCreate(ctx, req, userId);
|
||||
return;
|
||||
}
|
||||
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, error("Use GET or POST."));
|
||||
return;
|
||||
}
|
||||
|
||||
String badgeId = trailing;
|
||||
CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager();
|
||||
if (!manager.isCustomBadgeId(badgeId)) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Invalid badge id."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method() == HttpMethod.PUT || req.method() == HttpMethod.POST) {
|
||||
handleUpdate(ctx, req, userId, badgeId);
|
||||
return;
|
||||
}
|
||||
if (req.method() == HttpMethod.DELETE) {
|
||||
handleDelete(ctx, req, userId, badgeId);
|
||||
return;
|
||||
}
|
||||
sendJson(ctx, req, HttpResponseStatus.METHOD_NOT_ALLOWED, error("Use PUT or DELETE."));
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[badges/custom] unexpected error path=" + path, e);
|
||||
sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, error("Server error."));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTexts(ChannelHandlerContext ctx, FullHttpRequest req) {
|
||||
CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager();
|
||||
long version = manager.getTextCacheVersion();
|
||||
JsonObject ok = cachedTextsResponse;
|
||||
if (ok == null || cachedTextsVersion != version) {
|
||||
java.util.Map<String, CustomBadgeManager.BadgeText> cache = manager.getTextCache();
|
||||
JsonObject texts = new JsonObject();
|
||||
for (java.util.Map.Entry<String, CustomBadgeManager.BadgeText> entry : cache.entrySet()) {
|
||||
String badgeId = entry.getKey();
|
||||
CustomBadgeManager.BadgeText value = entry.getValue();
|
||||
texts.addProperty("badge_name_" + badgeId, value.name);
|
||||
texts.addProperty("badge_desc_" + badgeId, value.description);
|
||||
}
|
||||
JsonObject built = new JsonObject();
|
||||
built.add("texts", texts);
|
||||
built.addProperty("count", cache.size());
|
||||
built.addProperty("version", version);
|
||||
cachedTextsResponse = built;
|
||||
cachedTextsVersion = version;
|
||||
ok = built;
|
||||
}
|
||||
sendJsonCached(ctx, req, HttpResponseStatus.OK, ok);
|
||||
}
|
||||
|
||||
private void handleList(ChannelHandlerContext ctx, FullHttpRequest req, int userId) {
|
||||
CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager();
|
||||
List<CustomBadge> badges = manager.listForUser(userId);
|
||||
|
||||
JsonArray arr = new JsonArray();
|
||||
for (CustomBadge b : badges) arr.add(toJson(b, manager));
|
||||
|
||||
JsonObject ok = new JsonObject();
|
||||
ok.add("badges", arr);
|
||||
ok.addProperty("max", CustomBadgeManager.MAX_PER_USER);
|
||||
ok.addProperty("badgeWidth", CustomBadgeManager.BADGE_WIDTH);
|
||||
ok.addProperty("badgeHeight", CustomBadgeManager.BADGE_HEIGHT);
|
||||
ok.addProperty("maxBadgeSizeBytes", CustomBadgeManager.MAX_BADGE_SIZE_BYTES);
|
||||
if (manager.getSettings() != null) {
|
||||
ok.addProperty("priceBadge", manager.getSettings().getPriceBadge());
|
||||
ok.addProperty("currencyType", manager.getSettings().getCurrencyType());
|
||||
}
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
}
|
||||
|
||||
private void handleCreate(ChannelHandlerContext ctx, FullHttpRequest req, int userId) {
|
||||
JsonObject body = readJsonBody(req);
|
||||
if (body == null) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Invalid JSON body."));
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] png = decodeImage(body);
|
||||
if (png == null) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Missing or invalid image."));
|
||||
return;
|
||||
}
|
||||
|
||||
String name = optString(body, "name");
|
||||
String description = optString(body, "description");
|
||||
|
||||
CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager();
|
||||
try {
|
||||
CustomBadge created = manager.create(userId, name, description, png);
|
||||
sendJson(ctx, req, HttpResponseStatus.CREATED, toJson(created, manager));
|
||||
} catch (CustomBadgeException e) {
|
||||
sendJson(ctx, req, statusFor(e), error(e.getMessage(), e.getCode()));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUpdate(ChannelHandlerContext ctx, FullHttpRequest req, int userId, String badgeId) {
|
||||
JsonObject body = readJsonBody(req);
|
||||
if (body == null) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Invalid JSON body."));
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] png = decodeImage(body);
|
||||
if (png == null) {
|
||||
sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, error("Missing or invalid image."));
|
||||
return;
|
||||
}
|
||||
|
||||
String name = optString(body, "name");
|
||||
String description = optString(body, "description");
|
||||
|
||||
CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager();
|
||||
try {
|
||||
CustomBadge updated = manager.update(userId, badgeId, name, description, png);
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, toJson(updated, manager));
|
||||
} catch (CustomBadgeException e) {
|
||||
sendJson(ctx, req, statusFor(e), error(e.getMessage(), e.getCode()));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDelete(ChannelHandlerContext ctx, FullHttpRequest req, int userId, String badgeId) {
|
||||
CustomBadgeManager manager = Emulator.getGameEnvironment().getCustomBadgeManager();
|
||||
try {
|
||||
manager.delete(userId, badgeId);
|
||||
JsonObject ok = new JsonObject();
|
||||
ok.addProperty("deleted", badgeId);
|
||||
sendJson(ctx, req, HttpResponseStatus.OK, ok);
|
||||
} catch (CustomBadgeException e) {
|
||||
sendJson(ctx, req, statusFor(e), error(e.getMessage(), e.getCode()));
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decodeImage(JsonObject body) {
|
||||
if (!body.has("image")) return null;
|
||||
try {
|
||||
String raw = body.get("image").getAsString();
|
||||
if (raw == null || raw.isEmpty()) return null;
|
||||
int comma = raw.indexOf(',');
|
||||
String b64 = raw.startsWith("data:") && comma >= 0 ? raw.substring(comma + 1) : raw;
|
||||
return Base64.getDecoder().decode(b64.replaceAll("\\s+", ""));
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonObject readJsonBody(FullHttpRequest req) {
|
||||
try {
|
||||
String text = req.content().toString(StandardCharsets.UTF_8);
|
||||
if (text.isEmpty()) return new JsonObject();
|
||||
return JsonParser.parseString(text).getAsJsonObject();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String optString(JsonObject body, String key) {
|
||||
if (body == null || !body.has(key) || body.get(key).isJsonNull()) return "";
|
||||
try { return body.get(key).getAsString(); }
|
||||
catch (Exception e) { return ""; }
|
||||
}
|
||||
|
||||
private static int authenticate(FullHttpRequest req) {
|
||||
String header = req.headers().get(HttpHeaderNames.AUTHORIZATION);
|
||||
if (header == null || header.isEmpty()) return 0;
|
||||
String token;
|
||||
if (header.startsWith("Bearer ")) token = header.substring(7).trim();
|
||||
else token = header.trim();
|
||||
return AccessTokenService.verify(token);
|
||||
}
|
||||
|
||||
private static HttpResponseStatus statusFor(CustomBadgeException e) {
|
||||
return switch (e.getCode()) {
|
||||
case "not_found" -> HttpResponseStatus.NOT_FOUND;
|
||||
case "insufficient_funds" -> HttpResponseStatus.PAYMENT_REQUIRED;
|
||||
case "must_be_online" -> HttpResponseStatus.CONFLICT;
|
||||
case "rate_limited" -> HttpResponseStatus.TOO_MANY_REQUESTS;
|
||||
case "limit_reached", "wrong_dimensions", "too_large", "empty", "invalid_image", "not_configured" ->
|
||||
HttpResponseStatus.BAD_REQUEST;
|
||||
default -> HttpResponseStatus.INTERNAL_SERVER_ERROR;
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonObject toJson(CustomBadge badge, CustomBadgeManager manager) {
|
||||
JsonObject obj = new JsonObject();
|
||||
obj.addProperty("badgeId", badge.getBadgeId());
|
||||
obj.addProperty("badgeCode", badge.getBadgeId());
|
||||
obj.addProperty("name", badge.getBadgeName());
|
||||
obj.addProperty("description", badge.getBadgeDescription());
|
||||
obj.addProperty("dateCreated", badge.getDateCreated());
|
||||
obj.addProperty("dateEdit", badge.getDateEdit());
|
||||
obj.addProperty("url", manager.publicUrlFor(badge.getBadgeId()));
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static JsonObject error(String message) {
|
||||
return error(message, null);
|
||||
}
|
||||
|
||||
private static JsonObject error(String message, String code) {
|
||||
JsonObject obj = new JsonObject();
|
||||
obj.addProperty("error", message);
|
||||
if (code != null) obj.addProperty("code", code);
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static void sendJsonCached(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);
|
||||
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);
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
|
||||
private static void sendCors(ChannelHandlerContext ctx, FullHttpRequest req) {
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(
|
||||
HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT);
|
||||
applyCors(req, response);
|
||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
|
||||
private static void applyCors(FullHttpRequest req, FullHttpResponse response) {
|
||||
String origin = req.headers().get(HttpHeaderNames.ORIGIN);
|
||||
if (origin != null && !origin.isEmpty()) {
|
||||
response.headers().set("Access-Control-Allow-Origin", origin);
|
||||
response.headers().set("Vary", "Origin");
|
||||
response.headers().set("Access-Control-Allow-Credentials", "true");
|
||||
}
|
||||
response.headers().set("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, OPTIONS");
|
||||
response.headers().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
|
||||
}
|
||||
|
||||
private static boolean isKeepAlive(FullHttpRequest req) {
|
||||
String connection = req.headers().get(HttpHeaderNames.CONNECTION);
|
||||
if (connection != null && connection.equalsIgnoreCase("close")) return false;
|
||||
if (connection != null && connection.equalsIgnoreCase("keep-alive")) return true;
|
||||
return req.protocolVersion().isKeepAliveDefault();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static String resolveClientIp(ChannelHandlerContext ctx, FullHttpRequest req) {
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package com.eu.habbo.networking.gameserver.crypto;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.util.Base64;
|
||||
|
||||
public final class CryptoSigningKeyManager {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CryptoSigningKeyManager.class);
|
||||
private static final String KEY_PUBLIC = "crypto.ws.signing.public_key";
|
||||
private static final String KEY_PRIVATE = "crypto.ws.signing.private_key";
|
||||
private static volatile KeyPair cached;
|
||||
private static volatile String cachedPublicB64;
|
||||
private CryptoSigningKeyManager() {}
|
||||
|
||||
public static KeyPair get() {
|
||||
KeyPair kp = cached;
|
||||
if (kp != null) return kp;
|
||||
|
||||
synchronized (CryptoSigningKeyManager.class) {
|
||||
if (cached != null) return cached;
|
||||
|
||||
String pubB64 = Emulator.getConfig().getValue(KEY_PUBLIC, "");
|
||||
String privB64 = Emulator.getConfig().getValue(KEY_PRIVATE, "");
|
||||
|
||||
if (pubB64 != null && !pubB64.isEmpty() && privB64 != null && !privB64.isEmpty()) {
|
||||
try {
|
||||
byte[] pubDer = Base64.getDecoder().decode(pubB64);
|
||||
byte[] privDer = Base64.getDecoder().decode(privB64);
|
||||
KeyFactory kf = KeyFactory.getInstance("EC");
|
||||
PublicKey pub = kf.generatePublic(new X509EncodedKeySpec(pubDer));
|
||||
PrivateKey priv = kf.generatePrivate(new PKCS8EncodedKeySpec(privDer));
|
||||
cached = new KeyPair(pub, priv);
|
||||
cachedPublicB64 = pubB64;
|
||||
return cached;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[ws-crypto] persisted signing key is corrupt, generating a new pair", e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
KeyPair generated = WsSessionCrypto.generateSigningKeyPair();
|
||||
byte[] pubDer = WsSessionCrypto.encodePublicKeySpki(generated.getPublic());
|
||||
byte[] privDer = WsSessionCrypto.encodePrivateKeyPkcs8(generated.getPrivate());
|
||||
String newPubB64 = Base64.getEncoder().withoutPadding().encodeToString(pubDer);
|
||||
String newPrivB64 = Base64.getEncoder().withoutPadding().encodeToString(privDer);
|
||||
|
||||
persist(KEY_PUBLIC, newPubB64);
|
||||
persist(KEY_PRIVATE, newPrivB64);
|
||||
Emulator.getConfig().update(KEY_PUBLIC, newPubB64);
|
||||
Emulator.getConfig().update(KEY_PRIVATE, newPrivB64);
|
||||
|
||||
cached = generated;
|
||||
cachedPublicB64 = newPubB64;
|
||||
LOGGER.info("[ws-crypto] generated a new ECDSA P-256 signing keypair (persisted to emulator_settings)");
|
||||
return cached;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Cannot generate signing keypair", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String publicKeyBase64() {
|
||||
if (cachedPublicB64 == null) get();
|
||||
return cachedPublicB64;
|
||||
}
|
||||
|
||||
private static void persist(String key, String value) {
|
||||
try (Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(
|
||||
"INSERT INTO emulator_settings (`key`, `value`) VALUES (?, ?) "
|
||||
+ "ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)")) {
|
||||
stmt.setString(1, key);
|
||||
stmt.setString(2, value);
|
||||
stmt.executeUpdate();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[ws-crypto] failed to persist " + key + " to emulator_settings (key stays in-memory only)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.eu.habbo.networking.gameserver.crypto;
|
||||
|
||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WsAesDecoder extends MessageToMessageDecoder<ByteBuf> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WsAesDecoder.class);
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
|
||||
byte[] key = ctx.channel().attr(GameServerAttributes.WS_AES_KEY).get();
|
||||
if (key == null) {
|
||||
LOGGER.warn("[ws-crypto] inbound frame with no session key, closing");
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
int readable = in.readableBytes();
|
||||
if (readable < WsSessionCrypto.NONCE_LEN + 16) {
|
||||
LOGGER.warn("[ws-crypto] inbound frame too short ({} bytes)", readable);
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] nonce = new byte[WsSessionCrypto.NONCE_LEN];
|
||||
in.readBytes(nonce);
|
||||
|
||||
byte[] ct = new byte[in.readableBytes()];
|
||||
in.readBytes(ct);
|
||||
|
||||
try {
|
||||
byte[] plain = WsSessionCrypto.aesGcmDecrypt(key, nonce, ct);
|
||||
out.add(Unpooled.wrappedBuffer(plain));
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("[ws-crypto] AES-GCM decrypt failed ({}), closing channel", e.getClass().getSimpleName());
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.eu.habbo.networking.gameserver.crypto;
|
||||
|
||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToMessageEncoder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WsAesEncoder extends MessageToMessageEncoder<ByteBuf> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WsAesEncoder.class);
|
||||
|
||||
@Override
|
||||
protected void encode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
|
||||
byte[] key = ctx.channel().attr(GameServerAttributes.WS_AES_KEY).get();
|
||||
if (key == null) {
|
||||
LOGGER.warn("[ws-crypto] outbound frame with no session key, dropping");
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] plain = new byte[in.readableBytes()];
|
||||
in.readBytes(plain);
|
||||
|
||||
byte[] nonce = WsSessionCrypto.randomNonce();
|
||||
byte[] ct = WsSessionCrypto.aesGcmEncrypt(key, nonce, plain);
|
||||
|
||||
ByteBuf framed = ctx.alloc().buffer(nonce.length + ct.length);
|
||||
framed.writeBytes(nonce);
|
||||
framed.writeBytes(ct);
|
||||
out.add(framed);
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package com.eu.habbo.networking.gameserver.crypto;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
|
||||
public class WsHandshakeHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WsHandshakeHandler.class);
|
||||
public static final String HANDLER_NAME = "wsCryptoHandshake";
|
||||
private static final boolean SIGN_ENABLED = Emulator.getConfig().getBoolean("crypto.ws.signing.enabled", false);
|
||||
private KeyPair serverKeyPair;
|
||||
private boolean helloSent = false;
|
||||
private boolean handshakeComplete = false;
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||
if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
|
||||
sendServerHello(ctx);
|
||||
}
|
||||
super.userEventTriggered(ctx, evt);
|
||||
}
|
||||
|
||||
private void sendServerHello(ChannelHandlerContext ctx) {
|
||||
if (helloSent) return;
|
||||
try {
|
||||
this.serverKeyPair = WsSessionCrypto.generateEphemeralKeyPair();
|
||||
byte[] spki = WsSessionCrypto.encodePublicKeySpki(serverKeyPair.getPublic());
|
||||
byte[] sigIeee = null;
|
||||
if (SIGN_ENABLED) {
|
||||
KeyPair signingKp = CryptoSigningKeyManager.get();
|
||||
byte[] sigDer = WsSessionCrypto.signEcdsaSha256(signingKp.getPrivate(), spki);
|
||||
sigIeee = WsSessionCrypto.derToIeee1363(sigDer);
|
||||
}
|
||||
|
||||
int frameLen = 4 + 1 + 2 + spki.length + (sigIeee != null ? 2 + sigIeee.length : 0);
|
||||
ByteBuf buf = ctx.alloc().buffer(frameLen);
|
||||
buf.writeInt(WsSessionCrypto.HANDSHAKE_MAGIC);
|
||||
buf.writeByte(WsSessionCrypto.TYPE_SERVER_HELLO);
|
||||
buf.writeShort(spki.length);
|
||||
buf.writeBytes(spki);
|
||||
if (sigIeee != null) {
|
||||
buf.writeShort(sigIeee.length);
|
||||
buf.writeBytes(sigIeee);
|
||||
}
|
||||
|
||||
ctx.writeAndFlush(buf);
|
||||
helloSent = true;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[ws-crypto] failed to send server_hello", e);
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
if (handshakeComplete) {
|
||||
ctx.fireChannelRead(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(msg instanceof ByteBuf)) {
|
||||
ctx.fireChannelRead(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuf in = (ByteBuf) msg;
|
||||
try {
|
||||
if (in.readableBytes() < 7) {
|
||||
LOGGER.warn("[ws-crypto] handshake frame too short ({} bytes) from {}", in.readableBytes(), clientAddress(ctx));
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
int magic = in.readInt();
|
||||
if (magic != WsSessionCrypto.HANDSHAKE_MAGIC) {
|
||||
LOGGER.warn("[ws-crypto] handshake magic mismatch: 0x{} from {}", Integer.toHexString(magic), clientAddress(ctx));
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
byte type = in.readByte();
|
||||
if (type != WsSessionCrypto.TYPE_CLIENT_HELLO) {
|
||||
LOGGER.warn("[ws-crypto] expected client_hello, got type=0x{} from {}", Integer.toHexString(type & 0xff), clientAddress(ctx));
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
int keyLen = in.readUnsignedShort();
|
||||
if (keyLen <= 0 || keyLen > in.readableBytes() || keyLen > 2048) {
|
||||
LOGGER.warn("[ws-crypto] invalid client key length {} from {}", keyLen, clientAddress(ctx));
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] clientSpki = new byte[keyLen];
|
||||
in.readBytes(clientSpki);
|
||||
|
||||
PublicKey clientPub = WsSessionCrypto.decodePublicKeySpki(clientSpki);
|
||||
PrivateKey ourPriv = serverKeyPair.getPrivate();
|
||||
byte[] shared = WsSessionCrypto.deriveSharedSecret(ourPriv, clientPub);
|
||||
byte[] aesKey = WsSessionCrypto.deriveAesKey(shared);
|
||||
ctx.channel().attr(GameServerAttributes.WS_AES_KEY).set(aesKey);
|
||||
ChannelPipeline p = ctx.pipeline();
|
||||
p.addAfter(HANDLER_NAME, "wsAesDecoder", new WsAesDecoder());
|
||||
p.addAfter(HANDLER_NAME, "wsAesEncoder", new WsAesEncoder());
|
||||
handshakeComplete = true;
|
||||
p.remove(this);
|
||||
|
||||
LOGGER.debug("[ws-crypto] handshake complete for {}", clientAddress(ctx));
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("[ws-crypto] handshake failed from {} : {}", clientAddress(ctx), friendlyReason(e));
|
||||
ctx.close();
|
||||
} finally {
|
||||
in.release();
|
||||
}
|
||||
}
|
||||
|
||||
private static String clientAddress(ChannelHandlerContext ctx) {
|
||||
String wsIp = ctx.channel().attr(GameServerAttributes.WS_IP).get();
|
||||
if (wsIp != null && !wsIp.isEmpty()) return wsIp;
|
||||
return String.valueOf(ctx.channel().remoteAddress());
|
||||
}
|
||||
|
||||
private static String friendlyReason(Throwable t) {
|
||||
if (t == null) return "unknown";
|
||||
String name = t.getClass().getSimpleName();
|
||||
String msg = t.getMessage();
|
||||
return (msg == null || msg.isEmpty()) ? name : name + ": " + msg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
if (cause instanceof java.io.IOException) {
|
||||
LOGGER.debug("[ws-crypto] client disconnected during handshake ({}): {}",
|
||||
clientAddress(ctx), friendlyReason(cause));
|
||||
} else {
|
||||
LOGGER.error("[ws-crypto] handshake handler error from " + clientAddress(ctx), cause);
|
||||
}
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package com.eu.habbo.networking.gameserver.crypto;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyAgreement;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.*;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class WsSessionCrypto {
|
||||
|
||||
public static final int HANDSHAKE_MAGIC = 0xC0DEC0DE;
|
||||
public static final byte TYPE_SERVER_HELLO = 0x01;
|
||||
public static final byte TYPE_CLIENT_HELLO = 0x02;
|
||||
|
||||
public static final String HKDF_INFO = "nitro-ws-v1";
|
||||
public static final int AES_KEY_LEN = 32;
|
||||
public static final int NONCE_LEN = 12;
|
||||
public static final int GCM_TAG_BITS = 128;
|
||||
|
||||
private static final SecureRandom RNG = new SecureRandom();
|
||||
|
||||
private WsSessionCrypto() {}
|
||||
|
||||
public static KeyPair generateEphemeralKeyPair() throws GeneralSecurityException {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
|
||||
kpg.initialize(new java.security.spec.ECGenParameterSpec("secp256r1"), RNG);
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
public static byte[] encodePublicKeySpki(PublicKey publicKey) {
|
||||
return publicKey.getEncoded();
|
||||
}
|
||||
|
||||
public static PublicKey decodePublicKeySpki(byte[] spki) throws GeneralSecurityException {
|
||||
KeyFactory kf = KeyFactory.getInstance("EC");
|
||||
return kf.generatePublic(new X509EncodedKeySpec(spki));
|
||||
}
|
||||
|
||||
public static byte[] deriveSharedSecret(PrivateKey ourPrivate, PublicKey theirPublic) throws GeneralSecurityException {
|
||||
KeyAgreement ka = KeyAgreement.getInstance("ECDH");
|
||||
ka.init(ourPrivate);
|
||||
ka.doPhase(theirPublic, true);
|
||||
return ka.generateSecret();
|
||||
}
|
||||
|
||||
public static byte[] hkdfSha256(byte[] ikm, byte[] salt, byte[] info, int outLen) throws GeneralSecurityException {
|
||||
if (salt == null || salt.length == 0) salt = new byte[32];
|
||||
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(salt, "HmacSHA256"));
|
||||
byte[] prk = mac.doFinal(ikm);
|
||||
|
||||
int hashLen = 32;
|
||||
int n = (outLen + hashLen - 1) / hashLen;
|
||||
if (n > 255) throw new GeneralSecurityException("HKDF output too long");
|
||||
|
||||
ByteArrayOutputStream okm = new ByteArrayOutputStream();
|
||||
byte[] t = new byte[0];
|
||||
|
||||
for (int i = 1; i <= n; i++) {
|
||||
mac.init(new SecretKeySpec(prk, "HmacSHA256"));
|
||||
mac.update(t);
|
||||
if (info != null) mac.update(info);
|
||||
mac.update((byte) i);
|
||||
t = mac.doFinal();
|
||||
okm.write(t, 0, t.length);
|
||||
}
|
||||
|
||||
byte[] result = okm.toByteArray();
|
||||
return (result.length == outLen) ? result : Arrays.copyOf(result, outLen);
|
||||
}
|
||||
|
||||
public static byte[] deriveAesKey(byte[] sharedSecret) throws GeneralSecurityException {
|
||||
return hkdfSha256(sharedSecret, null, HKDF_INFO.getBytes(StandardCharsets.UTF_8), AES_KEY_LEN);
|
||||
}
|
||||
|
||||
public static byte[] aesGcmEncrypt(byte[] key, byte[] nonce, byte[] plaintext) throws GeneralSecurityException {
|
||||
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(GCM_TAG_BITS, nonce));
|
||||
return c.doFinal(plaintext);
|
||||
}
|
||||
|
||||
public static byte[] aesGcmDecrypt(byte[] key, byte[] nonce, byte[] ciphertextWithTag) throws GeneralSecurityException {
|
||||
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(GCM_TAG_BITS, nonce));
|
||||
return c.doFinal(ciphertextWithTag);
|
||||
}
|
||||
|
||||
public static byte[] randomNonce() {
|
||||
byte[] n = new byte[NONCE_LEN];
|
||||
RNG.nextBytes(n);
|
||||
return n;
|
||||
}
|
||||
|
||||
public static KeyPair generateSigningKeyPair() throws GeneralSecurityException {
|
||||
return generateEphemeralKeyPair();
|
||||
}
|
||||
|
||||
public static PrivateKey decodePrivateKeyPkcs8(byte[] pkcs8) throws GeneralSecurityException {
|
||||
KeyFactory kf = KeyFactory.getInstance("EC");
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8));
|
||||
}
|
||||
|
||||
public static byte[] encodePrivateKeyPkcs8(PrivateKey privateKey) {
|
||||
return privateKey.getEncoded();
|
||||
}
|
||||
|
||||
public static byte[] signEcdsaSha256(PrivateKey signingKey, byte[] message) throws GeneralSecurityException {
|
||||
Signature sig = Signature.getInstance("SHA256withECDSA");
|
||||
sig.initSign(signingKey);
|
||||
sig.update(message);
|
||||
return sig.sign();
|
||||
}
|
||||
|
||||
public static byte[] derToIeee1363(byte[] der) throws GeneralSecurityException {
|
||||
if (der == null || der.length < 8 || der[0] != 0x30) {
|
||||
throw new GeneralSecurityException("Malformed DER signature");
|
||||
}
|
||||
|
||||
int seqLen;
|
||||
int idx;
|
||||
if ((der[1] & 0x80) == 0) {
|
||||
seqLen = der[1] & 0xff;
|
||||
idx = 2;
|
||||
} else {
|
||||
int lenBytes = der[1] & 0x7f;
|
||||
if (lenBytes > 2) throw new GeneralSecurityException("DER length too big");
|
||||
seqLen = 0;
|
||||
for (int i = 0; i < lenBytes; i++) seqLen = (seqLen << 8) | (der[2 + i] & 0xff);
|
||||
idx = 2 + lenBytes;
|
||||
}
|
||||
if (idx + seqLen > der.length) throw new GeneralSecurityException("DER truncated");
|
||||
|
||||
if (der[idx] != 0x02) throw new GeneralSecurityException("Expected INTEGER r");
|
||||
int rLen = der[idx + 1] & 0xff;
|
||||
int rStart = idx + 2;
|
||||
|
||||
int sHeader = rStart + rLen;
|
||||
if (der[sHeader] != 0x02) throw new GeneralSecurityException("Expected INTEGER s");
|
||||
int sLen = der[sHeader + 1] & 0xff;
|
||||
int sStart = sHeader + 2;
|
||||
|
||||
byte[] r = stripLeadingZero(Arrays.copyOfRange(der, rStart, rStart + rLen));
|
||||
byte[] s = stripLeadingZero(Arrays.copyOfRange(der, sStart, sStart + sLen));
|
||||
|
||||
byte[] out = new byte[64];
|
||||
System.arraycopy(r, 0, out, 32 - r.length, r.length);
|
||||
System.arraycopy(s, 0, out, 64 - s.length, s.length);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static byte[] stripLeadingZero(byte[] v) {
|
||||
int i = 0;
|
||||
while (i < v.length - 1 && v[i] == 0x00) i++;
|
||||
return Arrays.copyOfRange(v, i, v.length);
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -2,7 +2,6 @@ package com.eu.habbo.networking.gameserver.decoders;
|
||||
|
||||
import com.eu.habbo.messages.ClientMessage;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
|
||||
@@ -12,8 +11,7 @@ public class GameByteDecoder extends ByteToMessageDecoder {
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
||||
short header = in.readShort();
|
||||
ByteBuf body = Unpooled.copiedBuffer(in.readBytes(in.readableBytes()));
|
||||
|
||||
ByteBuf body = in.readBytes(in.readableBytes());
|
||||
out.add(new ClientMessage(header, body));
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -8,6 +8,9 @@ db.params=
|
||||
db.pool.minsize=25
|
||||
db.pool.maxsize=100
|
||||
|
||||
# Encrypt your traffic
|
||||
crypto.ws.enabled=0
|
||||
|
||||
#Game Configuration.
|
||||
#Host IP. Most likely just 0.0.0.0 Use 127.0.0.1 if you want to play on LAN.
|
||||
game.host=0.0.0.0
|
||||
|
||||
Reference in New Issue
Block a user