diff --git a/Database Updates/000_all_database_updates.sql b/Database Updates/000_all_database_updates.sql index 38f3dc93..1795a9bb 100644 --- a/Database Updates/000_all_database_updates.sql +++ b/Database Updates/000_all_database_updates.sql @@ -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; diff --git a/Database Updates/013_UI_Client_News.sql b/Database Updates/013_UI_Client_News.sql new file mode 100644 index 00000000..84baffce --- /dev/null +++ b/Database Updates/013_UI_Client_News.sql @@ -0,0 +1,43 @@ +-- -------------------------------------------------------- +-- Host: 192.168.0.8 +-- Server version: 12.3.1-MariaDB-ubu2404 - mariadb.org binary distribution +-- Server OS: debian-linux-gnu +-- HeidiSQL Version: 12.13.0.7147 +-- -------------------------------------------------------- + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET NAMES utf8 */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- Dumping structure for table camwijsnew.ui_news +DROP TABLE IF EXISTS `ui_news`; +CREATE TABLE IF NOT EXISTS `ui_news` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `title` varchar(150) NOT NULL, + `body` text NOT NULL, + `image` mediumtext DEFAULT NULL, + `link_text` varchar(80) NOT NULL DEFAULT '', + `link_url` varchar(255) NOT NULL DEFAULT '', + `enabled` tinyint(1) NOT NULL DEFAULT 1, + `sort_order` int(11) NOT NULL DEFAULT 0, + `created_at` timestamp NOT NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`) USING BTREE, + KEY `ui_news_enabled_sort` (`enabled`,`sort_order`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +-- Dumping data for table camwijsnew.ui_news: ~2 rows (approximately) +DELETE FROM `ui_news`; +INSERT INTO `ui_news` (`id`, `title`, `body`, `image`, `link_text`, `link_url`, `enabled`, `sort_order`, `created_at`) VALUES + (1, 'Welcome to the Hotel!', 'Catch up on the latest events, updates and competitions happening right now in the hotel.', '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', '', '', 1, 0, '2026-04-30 14:30:18'), + (2, 'Want to join ?', 'Just create an account and start playing !', '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', '', '', 1, 1, '2026-04-30 14:30:19'); + +/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */; +/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */; +/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */; diff --git a/Database Updates/014_Make_Custom_Badge.sql b/Database Updates/014_Make_Custom_Badge.sql new file mode 100644 index 00000000..3fc97157 --- /dev/null +++ b/Database Updates/014_Make_Custom_Badge.sql @@ -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; \ No newline at end of file diff --git a/Database Updates/015_add_users_background_card_id.sql b/Database Updates/015_add_users_background_card_id.sql new file mode 100644 index 00000000..86b27393 --- /dev/null +++ b/Database Updates/015_add_users_background_card_id.sql @@ -0,0 +1 @@ +ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `background_card_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_overlay_id`; diff --git a/Emulator/sqlupdates/custom_prefixes_setup.sql b/Database Updates/016_custom_prefixes_setup.sql similarity index 79% rename from Emulator/sqlupdates/custom_prefixes_setup.sql rename to Database Updates/016_custom_prefixes_setup.sql index dd27c7d0..3cbd5c6a 100644 --- a/Emulator/sqlupdates/custom_prefixes_setup.sql +++ b/Database Updates/016_custom_prefixes_setup.sql @@ -314,4 +314,43 @@ INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES -- This setup does not require rows in `catalog_pages`. -- -- Command texts / permission inserts are intentionally omitted --- for compatibility with both legacy and normalized permission schemas. \ No newline at end of file +-- for compatibility with both legacy and normalized permission schemas. + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES + -- GivePrefix command + ('commands.keys.cmd_give_prefix', 'giveprefix'), + ('commands.error.cmd_give_prefix.usage', 'Usage: :giveprefix [icon] [effect]'), + ('commands.error.cmd_give_prefix.invalid_color', 'Invalid color format. Use hex format (#FF0000).'), + ('commands.error.cmd_give_prefix.too_long', 'Prefix text is too long (max 15 characters).'), + ('commands.error.cmd_give_prefix.user_not_found', 'User not found or not online.'), + ('commands.succes.cmd_give_prefix', 'Prefix {%prefix%} successfully given to %user%!'), + -- ListPrefixes command + ('commands.keys.cmd_list_prefixes', 'listprefixes'), + ('commands.error.cmd_list_prefixes.usage', 'Usage: :listprefixes '), + ('commands.error.cmd_list_prefixes.user_not_found', 'User not found or not online.'), + ('commands.succes.cmd_list_prefixes.header', 'Prefixes of %user%:'), + ('commands.succes.cmd_list_prefixes.empty', '%user% has no prefixes.'), + -- RemovePrefix command + ('commands.keys.cmd_remove_prefix', 'removeprefix'), + ('commands.error.cmd_remove_prefix.usage', 'Usage: :removeprefix '), + ('commands.error.cmd_remove_prefix.user_not_found', 'User not found or not online.'), + ('commands.error.cmd_remove_prefix.invalid_id', 'Invalid prefix ID. Must be a number or "all".'), + ('commands.error.cmd_remove_prefix.not_found', 'Prefix not found for this user.'), + ('commands.succes.cmd_remove_prefix', 'Prefix #%id% removed from %user%.'), + ('commands.succes.cmd_remove_prefix.all', 'All prefixes removed from %user%.'), + -- PrefixBlacklist command + ('commands.keys.cmd_prefix_blacklist', 'prefixblacklist'), + ('commands.error.cmd_prefix_blacklist.usage', 'Usage: :prefixblacklist [word]'), + ('commands.error.cmd_prefix_blacklist.empty_word', 'Word cannot be empty.'), + ('commands.succes.cmd_prefix_blacklist.header', 'Blacklisted prefix words:'), + ('commands.succes.cmd_prefix_blacklist.empty', 'No blacklisted words.'), + ('commands.succes.cmd_prefix_blacklist.added', 'Word "%word%" added to prefix blacklist.'), + ('commands.succes.cmd_prefix_blacklist.removed', 'Word "%word%" removed from prefix blacklist.'); + +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'); diff --git a/Database Updates/017_Profile_Backgrounds.sql b/Database Updates/017_Profile_Backgrounds.sql new file mode 100644 index 00000000..f68f93b5 --- /dev/null +++ b/Database Updates/017_Profile_Backgrounds.sql @@ -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); \ No newline at end of file diff --git a/Emulator/sqlupdates/custom_nick_icons_setup.sql b/Database Updates/custom_nick_icons_setup.sql similarity index 100% rename from Emulator/sqlupdates/custom_nick_icons_setup.sql rename to Database Updates/custom_nick_icons_setup.sql diff --git a/Emulator/sqlupdates/remember_login_tokens.sql b/Database Updates/remember_login_tokens.sql similarity index 100% rename from Emulator/sqlupdates/remember_login_tokens.sql rename to Database Updates/remember_login_tokens.sql diff --git a/Emulator/sqlupdates/wired_message_length_512.sql b/Database Updates/wired_message_length_512.sql similarity index 100% rename from Emulator/sqlupdates/wired_message_length_512.sql rename to Database Updates/wired_message_length_512.sql diff --git a/Emulator/pom.xml b/Emulator/pom.xml index 01b2888d..03f1c329 100644 --- a/Emulator/pom.xml +++ b/Emulator/pom.xml @@ -6,7 +6,7 @@ com.eu.habbo Habbo - 4.1.7 + 4.1.13 UTF-8 diff --git a/Emulator/sqlupdates/catalog_admin_permission.sql b/Emulator/sqlupdates/catalog_admin_permission.sql deleted file mode 100644 index 65eb459f..00000000 --- a/Emulator/sqlupdates/catalog_admin_permission.sql +++ /dev/null @@ -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 diff --git a/Emulator/src/main/java/com/eu/habbo/core/RoomUserPetComposer.java b/Emulator/src/main/java/com/eu/habbo/core/RoomUserPetComposer.java index 73d7992a..825f7dcf 100644 --- a/Emulator/src/main/java/com/eu/habbo/core/RoomUserPetComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/core/RoomUserPetComposer.java @@ -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()); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java index a3899b74..d8b8fe96 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java @@ -23,6 +23,8 @@ import com.eu.habbo.habbohotel.rooms.RoomChatBubbleManager; import com.eu.habbo.habbohotel.rooms.RoomManager; import com.eu.habbo.habbohotel.translations.GoogleTranslateManager; import com.eu.habbo.habbohotel.users.HabboManager; +import com.eu.habbo.habbohotel.users.custombadge.CustomBadgeManager; +import com.eu.habbo.habbohotel.users.infostand.InfostandBackgroundManager; import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionManager; import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionScheduler; import org.slf4j.Logger; @@ -60,6 +62,8 @@ public class GameEnvironment { private CalendarManager calendarManager; private RoomChatBubbleManager roomChatBubbleManager; private GoogleTranslateManager googleTranslateManager; + private CustomBadgeManager customBadgeManager; + private InfostandBackgroundManager infostandBackgroundManager; public void load() throws Exception { LOGGER.info("GameEnvironment -> Loading..."); @@ -87,6 +91,8 @@ public class GameEnvironment { this.calendarManager = new CalendarManager(); this.roomChatBubbleManager = new RoomChatBubbleManager(); this.googleTranslateManager = new GoogleTranslateManager(); + this.customBadgeManager = new CustomBadgeManager(); + this.infostandBackgroundManager = new InfostandBackgroundManager(); this.roomManager.loadPublicRooms(); this.navigatorManager.loadNavigator(); @@ -229,4 +235,12 @@ public class GameEnvironment { public GoogleTranslateManager getGoogleTranslateManager() { return this.googleTranslateManager; } + + public CustomBadgeManager getCustomBadgeManager() { + return this.customBadgeManager; + } + + public InfostandBackgroundManager getInfostandBackgroundManager() { + return this.infostandBackgroundManager; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java index f2724578..1f654d35 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java @@ -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; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Messenger.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Messenger.java index fa4603cf..f60c1b49 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Messenger.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Messenger.java @@ -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) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/StaffChatBuddy.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/StaffChatBuddy.java new file mode 100644 index 00000000..65140faa --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/StaffChatBuddy.java @@ -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 + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java index a40bdbf6..4769dd1d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java @@ -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 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 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 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 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 occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(), - item.getBaseItem().getLength(), rotation); + item.getBaseItem().getLength(), rotation); THashSet 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 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 occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(), - item.getBaseItem().getLength(), rotation); + item.getBaseItem().getLength(), rotation); THashSet 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 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 occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(), - item.getBaseItem().getLength(), rotation); + item.getBaseItem().getLength(), rotation); java.util.List>> 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; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInfo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInfo.java index e53aee11..0bcc8247 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInfo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInfo.java @@ -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); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadge.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadge.java new file mode 100644 index 00000000..c80c9be7 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadge.java @@ -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; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeException.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeException.java new file mode 100644 index 00000000..4a543e06 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeException.java @@ -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; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeManager.java new file mode 100644 index 00000000..bc1b2cad --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeManager.java @@ -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 rateBuckets = new ConcurrentHashMap<>(); + private final Map 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 getTextCache() { + return java.util.Collections.unmodifiableMap(this.textCache); + } + + public long getTextCacheVersion() { + return this.textCacheVersion.get(); + } + + private void loadTextCache() { + Map 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 listForUser(int userId) { + List 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 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 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(); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeSettings.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeSettings.java new file mode 100644 index 00000000..b7b82f86 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/custombadge/CustomBadgeSettings.java @@ -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; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/infostand/InfostandBackgroundManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/infostand/InfostandBackgroundManager.java new file mode 100644 index 00000000..60ce0419 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/infostand/InfostandBackgroundManager.java @@ -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> 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> 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 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; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/ActivateEffectEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/ActivateEffectEvent.java index 526da5ff..8c9fb6ae 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/ActivateEffectEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/ActivateEffectEvent.java @@ -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); } } \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/ChangeInfostandBgEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/ChangeInfostandBgEvent.java index 763bcb59..74485096 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/ChangeInfostandBgEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/ChangeInfostandBgEvent.java @@ -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)); } } -} \ No newline at end of file + + 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; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/pets/RoomPetComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/pets/RoomPetComposer.java index 2820706a..207a65fa 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/pets/RoomPetComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/pets/RoomPetComposer.java @@ -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 { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java index e352e9d4..7162dbf0 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserDataComposer.java @@ -24,6 +24,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()); UserCustomizationData customizationData = UserCustomizationData.fromHabbo(this.habbo); this.response.appendString(customizationData.nickIcon); this.response.appendString(customizationData.prefixText); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java index fdf6857c..796935c2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java @@ -44,6 +44,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()); @@ -87,6 +88,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()); @@ -128,6 +130,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()); @@ -160,6 +163,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()); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java index e7e5d859..e56326d4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserProfileComposer.java @@ -116,6 +116,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()); UserCustomizationData customizationData = (this.habbo != null) ? UserCustomizationData.fromHabbo(this.habbo) : UserCustomizationData.fromUserId(this.habboInfo.getId()); this.response.appendString(customizationData.nickIcon); this.response.appendString(customizationData.prefixText); diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java index 045b8cbd..facfb285 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/WebSocketChannelInitializer.java @@ -5,6 +5,7 @@ import com.eu.habbo.messages.PacketManager; import com.eu.habbo.networking.gameserver.auth.AuthHttpHandler; import com.eu.habbo.networking.gameserver.auth.NitroSecureApiHandler; import com.eu.habbo.networking.gameserver.auth.NitroSecureAssetHandler; +import com.eu.habbo.networking.gameserver.badges.BadgeHttpHandler; import com.eu.habbo.networking.gameserver.codec.WebSocketCodec; import com.eu.habbo.networking.gameserver.crypto.WsHandshakeHandler; import com.eu.habbo.networking.gameserver.decoders.*; @@ -57,6 +58,7 @@ public class WebSocketChannelInitializer extends ChannelInitializer 128) { + AuthRateLimiter.recordFailure(ip); + sendJson(ctx, req, HttpResponseStatus.BAD_REQUEST, errorPayload("Missing or invalid ssoTicket.")); + return; + } + + try (Connection conn = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement lookup = conn.prepareStatement( + "SELECT id, username FROM users WHERE auth_ticket = ? LIMIT 1")) { + lookup.setString(1, ssoTicket); + try (ResultSet rs = lookup.executeQuery()) { + if (!rs.next()) { + AuthRateLimiter.recordFailure(ip); + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, errorPayload("SSO ticket not recognised.")); + return; + } + int userId = rs.getInt("id"); + String username = rs.getString("username"); + + AuthRateLimiter.recordSuccess(ip); + + AccessTokenService.Issued access = AccessTokenService.issue(userId); + JsonObject ok = new JsonObject(); + ok.addProperty("username", username); + ok.addProperty("accessToken", access.token); + ok.addProperty("accessTokenExpiresAt", access.expiresAt); + sendJson(ctx, req, HttpResponseStatus.OK, ok); + } + } catch (Exception e) { + LOGGER.error("[auth/sso-token] lookup failed", e); + sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error.")); + } + } + private void handleRefresh(ChannelHandlerContext ctx, FullHttpRequest req, JsonObject body, String ip) { String jwt = readString(body, "rememberToken").trim(); if (jwt.isEmpty()) { @@ -349,6 +412,9 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { ok.addProperty("rememberToken", rot.jwt); ok.addProperty("expiresAt", rot.expiresAt); ok.addProperty("rememberExpiresAt", rot.expiresAt); + AccessTokenService.Issued access = AccessTokenService.issue(rot.userId); + ok.addProperty("accessToken", access.token); + ok.addProperty("accessTokenExpiresAt", access.expiresAt); sendJson(ctx, req, HttpResponseStatus.OK, ok); } catch (Exception e) { LOGGER.error("Refresh failed", e); @@ -366,62 +432,85 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { return; } - try (Connection conn = Emulator.getDatabase().getDataSource().getConnection(); - PreparedStatement stmt = conn.prepareStatement( - "SELECT id, username, password FROM users WHERE username = ? LIMIT 1")) { - stmt.setString(1, username); - try (ResultSet rs = stmt.executeQuery()) { - if (!rs.next()) { - LOGGER.info("[auth/login] user not found username='{}' ip={}", username, ip); - AuthRateLimiter.recordFailure(ip); - sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, - errorPayload("Invalid Habbo name or password.")); + try (Connection conn = Emulator.getDatabase().getDataSource().getConnection()) { + if (ip != null && !ip.isEmpty()) { + BanInfo ipBan = lookupIpBan(conn, ip); + if (ipBan != null) { + LOGGER.info("[auth/login] ip ban hit ip={} type={} expires={}", + ip, ipBan.type, ipBan.expiresAt); + sendJson(ctx, req, HttpResponseStatus.FORBIDDEN, bannedPayload(ipBan)); return; } + } - int userId = rs.getInt("id"); - String stored = rs.getString("password"); - String storedPreview = stored == null - ? "" - : (stored.isEmpty() ? "" : stored.substring(0, Math.min(7, stored.length())) + "…(" + stored.length() + " chars)"); - - if (stored == null || stored.isEmpty() || !checkPassword(password, stored)) { - LOGGER.info("[auth/login] password mismatch for user id={} username='{}' stored='{}'", - userId, username, storedPreview); - AuthRateLimiter.recordFailure(ip); - sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, - errorPayload("Invalid Habbo name or password.")); - return; - } - - String ssoTicket = mintSsoTicket(); - - try (PreparedStatement upd = conn.prepareStatement( - "UPDATE users SET auth_ticket = ?, ip_current = ? WHERE id = ? LIMIT 1")) { - upd.setString(1, ssoTicket); - upd.setString(2, ip == null ? "" : ip); - upd.setInt(3, userId); - upd.executeUpdate(); - } - - String rememberToken = null; - if (rememberMe) { - try { - RememberJwtService.RotationResult issued = RememberJwtService.issueForNewFamily( - conn, userId, rs.getString("username"), ip); - rememberToken = issued.jwt; - } catch (SQLException e) { - LOGGER.error("Failed to issue remember-me JWT for userId=" + userId, e); + try (PreparedStatement stmt = conn.prepareStatement( + "SELECT id, username, password FROM users WHERE username = ? LIMIT 1")) { + stmt.setString(1, username); + try (ResultSet rs = stmt.executeQuery()) { + if (!rs.next()) { + LOGGER.info("[auth/login] user not found username='{}' ip={}", username, ip); + AuthRateLimiter.recordFailure(ip); + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, + errorPayload("Invalid Habbo name or password.")); + return; } + + int userId = rs.getInt("id"); + String stored = rs.getString("password"); + String storedPreview = stored == null + ? "" + : (stored.isEmpty() ? "" : stored.substring(0, Math.min(7, stored.length())) + "…(" + stored.length() + " chars)"); + + if (stored == null || stored.isEmpty() || !checkPassword(password, stored)) { + LOGGER.info("[auth/login] password mismatch for user id={} username='{}' stored='{}'", + userId, username, storedPreview); + AuthRateLimiter.recordFailure(ip); + sendJson(ctx, req, HttpResponseStatus.UNAUTHORIZED, + errorPayload("Invalid Habbo name or password.")); + return; + } + + BanInfo accountBan = lookupAccountBan(conn, userId); + if (accountBan != null) { + LOGGER.info("[auth/login] account ban hit userId={} type={} expires={}", + userId, accountBan.type, accountBan.expiresAt); + AuthRateLimiter.recordSuccess(ip); + sendJson(ctx, req, HttpResponseStatus.FORBIDDEN, bannedPayload(accountBan)); + return; + } + + String ssoTicket = mintSsoTicket(); + + try (PreparedStatement upd = conn.prepareStatement( + "UPDATE users SET auth_ticket = ?, ip_current = ? WHERE id = ? LIMIT 1")) { + upd.setString(1, ssoTicket); + upd.setString(2, ip == null ? "" : ip); + upd.setInt(3, userId); + upd.executeUpdate(); + } + + String rememberToken = null; + if (rememberMe) { + try { + RememberJwtService.RotationResult issued = RememberJwtService.issueForNewFamily( + conn, userId, rs.getString("username"), ip); + rememberToken = issued.jwt; + } catch (SQLException e) { + LOGGER.error("Failed to issue remember-me JWT for userId=" + userId, e); + } + } + + AuthRateLimiter.recordSuccess(ip); + + JsonObject ok = new JsonObject(); + ok.addProperty("ssoTicket", ssoTicket); + ok.addProperty("username", rs.getString("username")); + if (rememberToken != null) ok.addProperty("rememberToken", rememberToken); + AccessTokenService.Issued access = AccessTokenService.issue(userId); + ok.addProperty("accessToken", access.token); + ok.addProperty("accessTokenExpiresAt", access.expiresAt); + sendJson(ctx, req, HttpResponseStatus.OK, ok); } - - AuthRateLimiter.recordSuccess(ip); - - JsonObject ok = new JsonObject(); - ok.addProperty("ssoTicket", ssoTicket); - ok.addProperty("username", rs.getString("username")); - if (rememberToken != null) ok.addProperty("rememberToken", rememberToken); - sendJson(ctx, req, HttpResponseStatus.OK, ok); } } catch (Exception e) { LOGGER.error("Login query failed for username=" + username, e); @@ -664,6 +753,76 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { sendJson(ctx, req, HttpResponseStatus.OK, res); } + private static final long NEWS_CACHE_TTL_MS = 30_000L; + private static final int NEWS_IMAGE_MAX_BYTES = 512 * 1024; + private static volatile NewsCacheEntry NEWS_CACHE = null; + + private static final class NewsCacheEntry { + final byte[] jsonBytes; + final long expiresAt; + NewsCacheEntry(byte[] j, long e) { jsonBytes = j; expiresAt = e; } + } + + private void handleNews(ChannelHandlerContext ctx, FullHttpRequest req) { + long now = System.currentTimeMillis(); + NewsCacheEntry cached = NEWS_CACHE; + + if (cached == null || cached.expiresAt < now) { + JsonArray items = new JsonArray(); + int limit = Math.max(1, Math.min(20, Emulator.getConfig().getInt("login.news.limit", 5))); + try (Connection conn = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement stmt = conn.prepareStatement( + "SELECT id, title, body, image, link_text, link_url " + + "FROM ui_news WHERE enabled = 1 " + + "ORDER BY sort_order ASC, id DESC LIMIT ?")) { + stmt.setInt(1, limit); + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + int id = rs.getInt("id"); + JsonObject n = new JsonObject(); + n.addProperty("id", id); + n.addProperty("title", rs.getString("title")); + n.addProperty("body", rs.getString("body")); + + String image = rs.getString("image"); + if (image != null && image.length() > NEWS_IMAGE_MAX_BYTES) { + LOGGER.warn("ui_news id={} image is {} bytes (>{}KB cap), omitting in response", + id, image.length(), NEWS_IMAGE_MAX_BYTES / 1024); + image = null; + } + n.addProperty("image", image); // gson encodes null as JSON null + + n.addProperty("linkText", rs.getString("link_text")); + n.addProperty("linkUrl", rs.getString("link_url")); + items.add(n); + } + } + } catch (Exception e) { + LOGGER.error("ui_news list failed", e); + sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Server error.")); + return; + } + + JsonObject res = new JsonObject(); + res.add("news", items); + byte[] bytes = res.toString().getBytes(StandardCharsets.UTF_8); + cached = new NewsCacheEntry(bytes, now + NEWS_CACHE_TTL_MS); + NEWS_CACHE = cached; + } + + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.OK, + Unpooled.wrappedBuffer(cached.jsonBytes)); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, cached.jsonBytes.length); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, "public, max-age=30"); + applyCors(req, response); + boolean keepAlive = isKeepAlive(req); + if (keepAlive) response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + var future = ctx.writeAndFlush(response); + if (!keepAlive) future.addListener(ChannelFutureListener.CLOSE); + } + private void handleServerKey(ChannelHandlerContext ctx, FullHttpRequest req) { try { JsonObject ok = new JsonObject(); @@ -806,6 +965,74 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter { sendJson(ctx, req, HttpResponseStatus.OK, ok); } + private static final long PERMANENT_BAN_THRESHOLD_SECONDS = 30L * 365L * 24L * 60L * 60L; + + private static final class BanInfo { + final String type; + final String reason; + final int expiresAt; + + BanInfo(String type, String reason, int expiresAt) { + this.type = type == null ? "account" : type; + this.reason = reason == null ? "" : reason; + this.expiresAt = expiresAt; + } + + boolean isPermanent() { + return (long) expiresAt - Emulator.getIntUnixTimestamp() > PERMANENT_BAN_THRESHOLD_SECONDS; + } + } + + private static BanInfo lookupAccountBan(Connection conn, int userId) throws SQLException { + try (PreparedStatement stmt = conn.prepareStatement( + "SELECT ban_expire, ban_reason, type FROM bans " + + "WHERE user_id = ? AND ban_expire >= ? AND (type = 'account' OR type = 'super') " + + "ORDER BY ban_expire DESC LIMIT 1")) { + stmt.setInt(1, userId); + stmt.setInt(2, Emulator.getIntUnixTimestamp()); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return new BanInfo(rs.getString("type"), rs.getString("ban_reason"), rs.getInt("ban_expire")); + } + } + } + return null; + } + + private static BanInfo lookupIpBan(Connection conn, String ip) throws SQLException { + try (PreparedStatement stmt = conn.prepareStatement( + "SELECT ban_expire, ban_reason, type FROM bans " + + "WHERE ip = ? AND ban_expire >= ? AND (type = 'ip' OR type = 'super') " + + "ORDER BY ban_expire DESC LIMIT 1")) { + stmt.setString(1, ip); + stmt.setInt(2, Emulator.getIntUnixTimestamp()); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return new BanInfo(rs.getString("type"), rs.getString("ban_reason"), rs.getInt("ban_expire")); + } + } + } + return null; + } + + private static JsonObject bannedPayload(BanInfo ban) { + boolean permanent = ban.isPermanent(); + String message = permanent + ? "Your account has been permanently banned." + : "Your account is temporarily banned."; + + JsonObject details = new JsonObject(); + details.addProperty("type", ban.type); + details.addProperty("reason", ban.reason); + details.addProperty("permanent", permanent); + if (!permanent) details.addProperty("expiresAt", ban.expiresAt); + + JsonObject obj = new JsonObject(); + obj.addProperty("error", message); + obj.add("ban", details); + return obj; + } + private static boolean checkPassword(String plain, String stored) { String compatible = stored.startsWith("$2y$") ? "$2a$" + stored.substring(4) : stored; try { diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/badges/BadgeHttpHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/badges/BadgeHttpHandler.java new file mode 100644 index 00000000..e06b9cae --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/badges/BadgeHttpHandler.java @@ -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 cache = manager.getTextCache(); + JsonObject texts = new JsonObject(); + for (java.util.Map.Entry 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 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 ""; + } +} diff --git a/Latest_Compiled_Version/Habbo-4.1.2-jar-with-dependencies.jar b/Latest_Compiled_Version/Habbo-4.1.2-jar-with-dependencies.jar index fa1e80db..c15dee6a 100644 Binary files a/Latest_Compiled_Version/Habbo-4.1.2-jar-with-dependencies.jar and b/Latest_Compiled_Version/Habbo-4.1.2-jar-with-dependencies.jar differ