diff --git a/.gitignore b/.gitignore index 5d9144a9..1517966d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ config.ini .DS_Store /Emulator/plugins +/Emulator/.idea diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 004a2fc0..00000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - 1710516016645 - - - - \ No newline at end of file diff --git a/Assets/bundled/generic/avatar_additions.nitro b/Assets/bundled/generic/avatar_additions.nitro deleted file mode 100644 index 0fb3ebff..00000000 Binary files a/Assets/bundled/generic/avatar_additions.nitro and /dev/null differ diff --git a/Assets/bundled/generic/floor_editor.nitro b/Assets/bundled/generic/floor_editor.nitro deleted file mode 100644 index 7c6674ab..00000000 Binary files a/Assets/bundled/generic/floor_editor.nitro and /dev/null differ diff --git a/Assets/bundled/generic/group_badge.nitro b/Assets/bundled/generic/group_badge.nitro deleted file mode 100644 index 9fb546dc..00000000 Binary files a/Assets/bundled/generic/group_badge.nitro and /dev/null differ diff --git a/Assets/bundled/generic/place_holder.nitro b/Assets/bundled/generic/place_holder.nitro deleted file mode 100644 index 78a97fca..00000000 Binary files a/Assets/bundled/generic/place_holder.nitro and /dev/null differ diff --git a/Assets/bundled/generic/place_holder_pet.nitro b/Assets/bundled/generic/place_holder_pet.nitro deleted file mode 100644 index c89aee1e..00000000 Binary files a/Assets/bundled/generic/place_holder_pet.nitro and /dev/null differ diff --git a/Assets/bundled/generic/place_holder_wall.nitro b/Assets/bundled/generic/place_holder_wall.nitro deleted file mode 100644 index 591c950f..00000000 Binary files a/Assets/bundled/generic/place_holder_wall.nitro and /dev/null differ diff --git a/Assets/bundled/generic/room.nitro b/Assets/bundled/generic/room.nitro deleted file mode 100644 index f4eafff0..00000000 Binary files a/Assets/bundled/generic/room.nitro and /dev/null differ diff --git a/Assets/bundled/generic/room_camwijs.nitro b/Assets/bundled/generic/room_camwijs.nitro deleted file mode 100644 index f4eafff0..00000000 Binary files a/Assets/bundled/generic/room_camwijs.nitro and /dev/null differ diff --git a/Assets/bundled/generic/selection_arrow.nitro b/Assets/bundled/generic/selection_arrow.nitro deleted file mode 100644 index 1a924d72..00000000 Binary files a/Assets/bundled/generic/selection_arrow.nitro and /dev/null differ diff --git a/Assets/bundled/generic/tile_cursor.nitro b/Assets/bundled/generic/tile_cursor.nitro deleted file mode 100644 index eb81ad2e..00000000 Binary files a/Assets/bundled/generic/tile_cursor.nitro and /dev/null differ diff --git a/Database Updates/000_all_database_updates.sql b/Database Updates/000_all_database_updates.sql new file mode 100644 index 00000000..38f3dc93 --- /dev/null +++ b/Database Updates/000_all_database_updates.sql @@ -0,0 +1,519 @@ +-- ============================================================================= +-- 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; +SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO'; + + +-- ============================================================================= +-- From: UpdateDatabase_Allow_diagonale.sql +-- ============================================================================= +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) +VALUES ('pathfinder.diagonal.enabled', '1'); + + +-- ============================================================================= +-- From: UpdateDatabase_BOT.sql +-- ============================================================================= +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) +VALUES ('hotel.bot.limit.walking.distance', '1'); + +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) +VALUES ('hotel.bot.limit.walking.distance.radius', '5'); + + +-- ============================================================================= +-- From: UpdateDatabase_Banners.sql +-- ============================================================================= +ALTER TABLE `users` + ADD COLUMN IF NOT EXISTS `background_id` INT(11) NOT NULL DEFAULT 0 AFTER `machine_id`, + ADD COLUMN IF NOT EXISTS `background_stand_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_id`, + ADD COLUMN IF NOT EXISTS `background_overlay_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_stand_id`; + + +-- ============================================================================= +-- From: UpdateDatabase_DanceCMD.sql +-- ============================================================================= +ALTER TABLE `permissions` + ADD COLUMN IF NOT EXISTS `cms_dance` ENUM('0','1') NULL DEFAULT '0' AFTER `cmd_credits`; + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) +VALUES ('commands.description.cmd_dance', 'dance around the world ! use 1 t/m 4 and 0 to stop'); + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) +VALUES ('commands.keys.cmd_dance', 'dance'); + + +-- ============================================================================= +-- From: UpdateDatabase_Happiness.sql +-- ============================================================================= +-- Rename key if the old one exists +UPDATE `emulator_texts` +SET `key` = 'generic.pet.happiness', `value` = 'Happiness' +WHERE `key` = 'generic.pet.happyness'; + +-- Rename columns (IF EXISTS prevents error if already renamed) +ALTER TABLE `pet_commands_data` + CHANGE COLUMN IF EXISTS `cost_happyness` `cost_happiness` int(11) NOT NULL DEFAULT '0'; + +ALTER TABLE `users_pets` + CHANGE COLUMN IF EXISTS `happyness` `happiness` int(11) NOT NULL DEFAULT '100'; + + +-- ============================================================================= +-- From: UpdateDatabase_Websocket.sql +-- ============================================================================= +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) +VALUES ('websockets.whitelist', 'localhost'); + +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) +VALUES ('ws.nitro.host', '0.0.0.0'); + +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) +VALUES ('ws.nitro.ip.header', ''); + +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) +VALUES ('ws.nitro.port', '2096'); + + +-- ============================================================================= +-- From: UpdateDatabase_unignorable.sql +-- ============================================================================= +ALTER TABLE `permissions` + ADD COLUMN IF NOT EXISTS `acc_unignorable` ENUM('0','1') NOT NULL DEFAULT '0'; + + +-- ============================================================================= +-- From: Default_Camera.sql +-- ============================================================================= +CREATE TABLE IF NOT EXISTS `camera_web` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `user_id` INT(11) NOT NULL, + `room_id` INT(11) NOT NULL DEFAULT 0, + `timestamp` INT(11) NOT NULL DEFAULT 0, + `url` VARCHAR(255) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + INDEX `idx_camera_web_user_id` (`user_id`), + INDEX `idx_camera_web_timestamp` (`timestamp`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Camera emulator settings +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('camera.url', 'http://localhost/camera/'); + +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('imager.location.output.camera', '/path/to/www/camera/'); + +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('imager.location.output.thumbnail', '/path/to/www/thumbnails/'); + +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('camera.item_id', '0'); + +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('camera.price.credits', '2'); + +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('camera.price.points', '0'); + +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('camera.price.points.publish', '1'); + +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('camera.extradata', '{"t":"%timestamp%","u":"%id%","m":"","s":"%room_id%","w":"%url%"}'); + +-- Camera emulator texts +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES +('camera.permission', 'You do not have permission to use the camera.'); + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES +('camera.wait', 'Please wait %seconds% more seconds before taking another photo.'); + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES +('camera.error.creation', 'An error occurred while processing your photo. Please try again.'); + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES +('camera.daily.limit', 'You have reached the daily photo limit. Try again tomorrow.'); + + +-- ============================================================================= +-- From: 07012026_UpdateDatabase_to_4-0-1.sql +-- ============================================================================= + +-- Wired abuse protection settings +INSERT INTO `emulator_settings` (`key`, `value`) VALUES +('wired.abuse.max.recursion.depth', '10') +ON DUPLICATE KEY UPDATE `key` = `key`; + +INSERT INTO `emulator_settings` (`key`, `value`) VALUES +('wired.abuse.max.events.per.window', '100') +ON DUPLICATE KEY UPDATE `key` = `key`; + +INSERT INTO `emulator_settings` (`key`, `value`) VALUES +('wired.abuse.rate.limit.window.ms', '10000') +ON DUPLICATE KEY UPDATE `key` = `key`; + +INSERT INTO `emulator_settings` (`key`, `value`) VALUES +('wired.abuse.ban.duration.ms', '600000') +ON DUPLICATE KEY UPDATE `key` = `key`; + +-- Wired abuse texts +INSERT INTO `emulator_texts` (`key`, `value`) VALUES +('wired.abuse.room.alert', 'Wired execution has been temporarily disabled in this room due to abuse detection. It will resume in %minutes% minutes.') +ON DUPLICATE KEY UPDATE `key` = `key`; + +INSERT INTO `emulator_texts` (`key`, `value`) VALUES +('wired.abuse.staff.title', 'Wired Abuse Detected') +ON DUPLICATE KEY UPDATE `key` = `key`; + +INSERT INTO `emulator_texts` (`key`, `value`) VALUES +('wired.abuse.staff.message', 'Room: %roomname%\nOwner: %owner%\nBanned for %minutes% minutes.') +ON DUPLICATE KEY UPDATE `key` = `key`; + +INSERT INTO `emulator_texts` (`key`, `value`) VALUES +('wired.abuse.staff.link', 'Go to Room') +ON DUPLICATE KEY UPDATE `key` = `key`; + +-- Wired tick resolution +INSERT INTO `emulator_settings` (`key`, `value`) VALUES +('wired.tick.resolution', '100') +ON DUPLICATE KEY UPDATE `key` = `key`; + +-- Wired engine configuration +INSERT INTO `emulator_settings` (`key`, `value`) VALUES +('wired.engine.enabled', '1'), +('wired.engine.exclusive', '1'), +('wired.engine.maxStepsPerStack', '100'), +('wired.engine.debug', '0') +ON DUPLICATE KEY UPDATE `key` = `key`; + +-- Wired tick system configuration +INSERT INTO `emulator_settings` (`key`, `value`) VALUES +('wired.tick.interval.ms', '50'), +('wired.tick.debug', '0'), +('wired.tick.thread.priority', '6') +ON DUPLICATE KEY UPDATE `key` = `key`; + +-- Pathfinder settings +INSERT INTO `emulator_settings` (`key`, `value`) VALUES +('pathfinder.click.delay', '0') +ON DUPLICATE KEY UPDATE `key` = `key`; + +INSERT INTO `emulator_settings` (`key`, `value`) VALUES +('pathfinder.retro-style.diagonals', '0') +ON DUPLICATE KEY UPDATE `key` = `key`; + +INSERT INTO `emulator_settings` (`key`, `value`) VALUES +('pathfinder.step.allow.falling', '1') +ON DUPLICATE KEY UPDATE `key` = `key`; + + +-- ============================================================================= +-- From: 09012026_UpdateDatabase_to_4-0-2.sql + 12012026_Battle Banzai.sql +-- (These two files are identical - deduplicated here) +-- ============================================================================= +INSERT INTO `emulator_settings` (`key`, `value`) VALUES +('hotel.banzai.fill.max_queue', '50'), +('hotel.banzai.fill.cooldown_ms', '100') +ON DUPLICATE KEY UPDATE `value` = VALUES(`value`); + + +-- ============================================================================= +-- From: 12012026_Breeding Fixes.sql +-- ============================================================================= + +-- Recreate pet_breeding with correct structure +CREATE TABLE IF NOT EXISTS `pet_breeding` ( + `pet_id` int(11) NOT NULL COMMENT 'Parent pet type', + `offspring_id` int(11) NOT NULL COMMENT 'Baby pet type', + PRIMARY KEY (`pet_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +TRUNCATE TABLE `pet_breeding`; + +INSERT INTO `pet_breeding` (`pet_id`, `offspring_id`) VALUES +(0, 29), -- Dog -> Baby Dog +(1, 28), -- Cat -> Baby Cat +(3, 25), -- Terrier -> Baby Terrier +(4, 24), -- Bear -> Baby Bear +(5, 30); -- Pig -> Baby Pig + +-- Recreate pet_breeding_races with correct structure +CREATE TABLE IF NOT EXISTS `pet_breeding_races` ( + `pet_type` int(11) NOT NULL COMMENT 'Baby pet type (offspring)', + `rarity_level` int(11) NOT NULL COMMENT '1=Common, 2=Uncommon, 3=Rare, 4=Epic', + `breed` int(11) NOT NULL COMMENT 'Visual breed/color variant', + PRIMARY KEY (`pet_type`, `rarity_level`, `breed`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +TRUNCATE TABLE `pet_breeding_races`; + +-- Baby Dog (29) - Offspring of Dog (0) +INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES +(29, 1, 0), (29, 1, 1), (29, 1, 2), (29, 1, 3), +(29, 1, 4), (29, 1, 5), (29, 1, 6), (29, 1, 7), +(29, 2, 8), (29, 2, 9), (29, 2, 10), (29, 2, 11), (29, 2, 12), +(29, 3, 13), (29, 3, 14), (29, 3, 15), (29, 3, 16), +(29, 4, 17), (29, 4, 18), (29, 4, 19); + +-- Baby Cat (28) - Offspring of Cat (1) +INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES +(28, 1, 0), (28, 1, 1), (28, 1, 2), (28, 1, 3), +(28, 1, 4), (28, 1, 5), (28, 1, 6), (28, 1, 7), +(28, 2, 8), (28, 2, 9), (28, 2, 10), (28, 2, 11), (28, 2, 12), +(28, 3, 13), (28, 3, 14), (28, 3, 15), (28, 3, 16), +(28, 4, 17), (28, 4, 18), (28, 4, 19); + +-- Baby Terrier (25) - Offspring of Terrier (3) +INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES +(25, 1, 0), (25, 1, 1), (25, 1, 2), (25, 1, 3), +(25, 1, 4), (25, 1, 5), (25, 1, 6), (25, 1, 7), +(25, 2, 8), (25, 2, 9), (25, 2, 10), (25, 2, 11), (25, 2, 12), +(25, 3, 13), (25, 3, 14), (25, 3, 15), (25, 3, 16), +(25, 4, 17), (25, 4, 18), (25, 4, 19); + +-- Baby Bear (24) - Offspring of Bear (4) +INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES +(24, 1, 0), (24, 1, 1), (24, 1, 2), (24, 1, 3), +(24, 1, 4), (24, 1, 5), (24, 1, 6), (24, 1, 7), +(24, 2, 8), (24, 2, 9), (24, 2, 10), (24, 2, 11), (24, 2, 12), +(24, 3, 13), (24, 3, 14), (24, 3, 15), (24, 3, 16), +(24, 4, 17), (24, 4, 18), (24, 4, 19); + +-- Baby Pig (30) - Offspring of Pig (5) +INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES +(30, 1, 0), (30, 1, 1), (30, 1, 2), (30, 1, 3), +(30, 1, 4), (30, 1, 5), (30, 1, 6), (30, 1, 7), +(30, 2, 8), (30, 2, 9), (30, 2, 10), (30, 2, 11), (30, 2, 12), +(30, 3, 13), (30, 3, 14), (30, 3, 15), (30, 3, 16), +(30, 4, 17), (30, 4, 18), (30, 4, 19); + +-- Fix pet_actions offspring_type values +UPDATE `pet_actions` SET `offspring_type` = 29 WHERE `pet_type` = 0; +UPDATE `pet_actions` SET `offspring_type` = 28 WHERE `pet_type` = 1; +UPDATE `pet_actions` SET `offspring_type` = 25 WHERE `pet_type` = 3; +UPDATE `pet_actions` SET `offspring_type` = 24 WHERE `pet_type` = 4; +UPDATE `pet_actions` SET `offspring_type` = 30 WHERE `pet_type` = 5; +UPDATE `pet_actions` SET `offspring_type` = -1 WHERE `pet_type` NOT IN (0, 1, 3, 4, 5); + +-- Fix items_base whitespace in interaction_type +UPDATE `items_base` SET `interaction_type` = TRIM(`interaction_type`); + +-- Ensure breeding nest items have correct interaction_type +UPDATE `items_base` SET `interaction_type` = 'breeding_nest' +WHERE `item_name` LIKE 'pet_breeding_%' AND `interaction_type` != 'breeding_nest'; + + +-- ============================================================================= +-- From: 12012026_ChatBubbles.sql +-- ============================================================================= +ALTER TABLE `permissions` + ADD COLUMN IF NOT EXISTS `cmd_update_chat_bubbles` ENUM('0','1') NOT NULL DEFAULT '0'; + +CREATE TABLE IF NOT EXISTS `chat_bubbles` ( + `type` INT(11) NOT NULL AUTO_INCREMENT COMMENT 'Only 46 and higher will work', + `name` VARCHAR(255) NOT NULL DEFAULT '', + `permission` VARCHAR(255) NOT NULL DEFAULT '', + `overridable` TINYINT(1) NOT NULL DEFAULT 1, + `triggers_talking_furniture` TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES +('commands.keys.cmd_update_chat_bubbles', 'update_chat_bubbles'); + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES +('commands.success.cmd_update_chat_bubbles', 'Successfully updated chat bubbles'); + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES +('commands.description.cmd_update_chat_bubbles', ':update_chat_bubbles'); + + +-- ============================================================================= +-- From: 16032026_updateall_command.sql +-- ============================================================================= +ALTER TABLE `permissions` + ADD COLUMN IF NOT EXISTS `cmd_update_all` ENUM('0','1') NOT NULL DEFAULT '0' AFTER `cmd_update_achievements`; + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES +('commands.keys.cmd_update_all', 'update_all'); + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES +('commands.description.cmd_update_all', ':update_all'); + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES +('commands.succes.cmd_update_all', 'Successfully updated everything!'); + + +-- ============================================================================= +-- From: 17032026_allow_underpass.sql +-- ============================================================================= +ALTER TABLE `rooms` + ADD COLUMN IF NOT EXISTS `allow_underpass` ENUM('0','1') NOT NULL DEFAULT '0' AFTER `move_diagonally`; + + +-- ============================================================================= +-- From: 19032026_hotel_timezone.sql +-- ============================================================================= +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) +VALUES ('hotel.timezone', 'Europe/Rome'); + + +-- ============================================================================= +-- From: 21022026_user_prefixes.sql +-- ============================================================================= +CREATE TABLE IF NOT EXISTS `user_prefixes` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `user_id` INT(11) NOT NULL, + `text` VARCHAR(50) NOT NULL, + `color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF', + `icon` VARCHAR(50) NOT NULL DEFAULT '', + `effect` VARCHAR(50) NOT NULL DEFAULT '', + `active` TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + INDEX `idx_user_id` (`user_id`), + INDEX `idx_user_active` (`user_id`, `active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +-- ============================================================================= +-- From: 06042026_builders_club_catalog_offers.sql +-- ============================================================================= +ALTER TABLE `catalog_club_offers` + MODIFY COLUMN `type` ENUM('HC','VIP','BUILDERS_CLUB','BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC'; + +ALTER TABLE `catalog_pages` + MODIFY COLUMN `page_layout` ENUM( + 'default_3x3', + 'club_buy', + 'club_gift', + 'frontpage', + 'spaces', + 'recycler', + 'recycler_info', + 'recycler_prizes', + 'trophies', + 'plasto', + 'marketplace', + 'marketplace_own_items', + 'spaces_new', + 'soundmachine', + 'guilds', + 'guild_furni', + 'info_duckets', + 'info_rentables', + 'info_pets', + 'roomads', + 'single_bundle', + 'sold_ltd_items', + 'badge_display', + 'bots', + 'pets', + 'pets2', + 'pets3', + 'productpage1', + 'room_bundle', + 'recent_purchases', + 'default_3x3_color_grouping', + 'guild_forum', + 'vip_buy', + 'info_loyalty', + 'loyalty_vip_buy', + 'collectibles', + 'petcustomization', + 'frontpage_featured', + 'builders_club_frontpage', + 'builders_club_addons', + 'builders_club_loyalty' + ) NOT NULL DEFAULT 'default_3x3'; + +ALTER TABLE `catalog_pages` + ADD COLUMN `catalog_mode` ENUM('NORMAL','BUILDER','BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `club_only`; + +ALTER TABLE `catalog_pages_bc` + MODIFY COLUMN `page_layout` ENUM( + 'default_3x3', + 'club_buy', + 'club_gift', + 'frontpage', + 'spaces', + 'recycler', + 'recycler_info', + 'recycler_prizes', + 'trophies', + 'plasto', + 'marketplace', + 'marketplace_own_items', + 'spaces_new', + 'soundmachine', + 'guilds', + 'guild_furni', + 'info_duckets', + 'info_rentables', + 'info_pets', + 'roomads', + 'single_bundle', + 'sold_ltd_items', + 'badge_display', + 'bots', + 'pets', + 'pets2', + 'pets3', + 'productpage1', + 'room_bundle', + 'recent_purchases', + 'default_3x3_color_grouping', + 'guild_forum', + 'vip_buy', + 'info_loyalty', + 'loyalty_vip_buy', + 'collectibles', + 'petcustomization', + 'frontpage_featured', + 'builders_club_frontpage', + 'builders_club_addons', + 'builders_club_loyalty' + ) NOT NULL DEFAULT 'default_3x3'; + +ALTER TABLE `users_settings` + ADD COLUMN IF NOT EXISTS `builders_club_bonus_furni` INT(11) NOT NULL DEFAULT 0 AFTER `hc_gifts_claimed`; + + +-- ============================================================================= +-- Done +-- ============================================================================= +SET FOREIGN_KEY_CHECKS = 1; +SET SQL_MODE = @OLD_SQL_MODE; diff --git a/Database Updates/001_optimize_gameserver.sql b/Database Updates/001_optimize_gameserver.sql new file mode 100644 index 00000000..ac8518a7 --- /dev/null +++ b/Database Updates/001_optimize_gameserver.sql @@ -0,0 +1,97 @@ +SET FOREIGN_KEY_CHECKS = 0; +SET @OLD_SQL_MODE = @@SQL_MODE; +SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO'; + +ALTER TABLE IF EXISTS `achievements` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; +ALTER TABLE IF EXISTS `items` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; +ALTER TABLE IF EXISTS `rooms` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; +ALTER TABLE IF EXISTS `users_achievements` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; +ALTER TABLE IF EXISTS `catalog_items` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; +ALTER TABLE IF EXISTS `catalog_pages` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; +ALTER TABLE IF EXISTS `navigator_flatcats` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; +ALTER TABLE IF EXISTS `wordfilter` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; +ALTER TABLE IF EXISTS `logs_shop_purchases` MODIFY `user_id` int(11) DEFAULT NULL; +ALTER TABLE IF EXISTS `users_subscriptions` MODIFY `user_id` int(11) DEFAULT NULL; +ALTER TABLE IF EXISTS `items` MODIFY `item_id` int(11) unsigned DEFAULT 0; + + +DELIMITER // +DROP PROCEDURE IF EXISTS `_add_id_pk_if_missing`// +CREATE PROCEDURE `_add_id_pk_if_missing`(IN tbl VARCHAR(64)) +BEGIN + DECLARE col_exists INT DEFAULT 0; + SELECT COUNT(*) INTO col_exists + FROM `information_schema`.`COLUMNS` + WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = tbl AND `COLUMN_NAME` = 'id'; + IF col_exists = 0 THEN + SET @sql = CONCAT('ALTER TABLE `', tbl, '` ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST'); + PREPARE stmt FROM @sql; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + END IF; +END// +DELIMITER ; + +CALL `_add_id_pk_if_missing`('bot_serves'); +CALL `_add_id_pk_if_missing`('chatlogs_room'); +CALL `_add_id_pk_if_missing`('commandlogs'); +CALL `_add_id_pk_if_missing`('room_enter_log'); + +DELIMITER // +DROP PROCEDURE IF EXISTS `_add_index_if_missing`// +CREATE PROCEDURE `_add_index_if_missing`(IN tbl VARCHAR(64), IN idx VARCHAR(64), IN cols VARCHAR(255)) +BEGIN + DECLARE tbl_exists INT DEFAULT 0; + DECLARE idx_exists INT DEFAULT 0; + SELECT COUNT(*) INTO tbl_exists FROM `information_schema`.`TABLES` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = tbl; + IF tbl_exists > 0 THEN + SELECT COUNT(*) INTO idx_exists FROM `information_schema`.`STATISTICS` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = tbl AND `INDEX_NAME` = idx; + IF idx_exists = 0 THEN + SET @sql = CONCAT('ALTER TABLE `', tbl, '` ADD INDEX `', idx, '` (', cols, ')'); + PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + END IF; + END IF; +END// +DELIMITER ; + +CALL `_add_index_if_missing`('bans', 'idx_bans_user_id', '`user_id`'); +CALL `_add_index_if_missing`('guilds', 'idx_guilds_user_id', '`user_id`'); +CALL `_add_index_if_missing`('room_bans', 'idx_room_bans_room_id', '`room_id`'); + + +DELIMITER // +DROP PROCEDURE IF EXISTS `_add_fk_if_missing`// +CREATE PROCEDURE `_add_fk_if_missing`(IN tbl VARCHAR(64), IN fk_name VARCHAR(64), IN col VARCHAR(64), IN ref_tbl VARCHAR(64), IN ref_col VARCHAR(64), IN on_del VARCHAR(20)) +BEGIN + DECLARE tbl_exists INT DEFAULT 0; + DECLARE ref_exists INT DEFAULT 0; + DECLARE fk_exists INT DEFAULT 0; + SELECT COUNT(*) INTO tbl_exists FROM `information_schema`.`TABLES` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = tbl; + SELECT COUNT(*) INTO ref_exists FROM `information_schema`.`TABLES` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = ref_tbl; + + IF tbl_exists > 0 AND ref_exists > 0 THEN + SELECT COUNT(*) INTO fk_exists FROM `information_schema`.`TABLE_CONSTRAINTS` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = tbl AND `CONSTRAINT_NAME` = fk_name; + IF fk_exists = 0 THEN + SET @sql = CONCAT('ALTER TABLE `', tbl, '` ADD CONSTRAINT `', fk_name, '` FOREIGN KEY (`', col, '`) REFERENCES `', ref_tbl, '` (`', ref_col, '`) ON DELETE ', on_del); + PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + END IF; + END IF; +END// +DELIMITER ; + +CALL `_add_fk_if_missing`('rooms', 'fk_rooms_owner', 'owner_id', 'users', 'id', 'CASCADE'); +CALL `_add_fk_if_missing`('items', 'fk_items_user', 'user_id', 'users', 'id', 'CASCADE'); +CALL `_add_fk_if_missing`('catalog_items', 'fk_catitems_page', 'page_id', 'catalog_pages', 'id', 'CASCADE'); +CALL `_add_fk_if_missing`('guilds', 'fk_guilds_user', 'user_id', 'users', 'id', 'CASCADE'); + +DROP PROCEDURE IF EXISTS `_add_id_pk_if_missing`; +DROP PROCEDURE IF EXISTS `_add_index_if_missing`; +DROP PROCEDURE IF EXISTS `_add_fk_if_missing`; + +# Make sure thenb System account exists +INSERT INTO `users` (`id`, `username`, `password`, `ip_register`, `ip_current`, `motto`, `look`, `rank`, `credits`) +SELECT 0, '[SYSTEM]', '!', '127.0.0.1', '127.0.0.1', 'System sentinel - do not delete', '', 1, 0 +WHERE NOT EXISTS (SELECT 1 FROM `users` WHERE `id` = 0); + +SET FOREIGN_KEY_CHECKS = 1; +SET SQL_MODE = @OLD_SQL_MODE; \ No newline at end of file diff --git a/Database Updates/002_forum_groups.sql b/Database Updates/002_forum_groups.sql new file mode 100644 index 00000000..fe077955 --- /dev/null +++ b/Database Updates/002_forum_groups.sql @@ -0,0 +1 @@ +ALTER TABLE `guild_forum_views` ADD UNIQUE KEY `user_guild` (`user_id`, `guild_id`); \ No newline at end of file diff --git a/Database Updates/002_move_wired_settings_to_wired_emulator_settings.sql b/Database Updates/002_move_wired_settings_to_wired_emulator_settings.sql new file mode 100644 index 00000000..a95755a3 --- /dev/null +++ b/Database Updates/002_move_wired_settings_to_wired_emulator_settings.sql @@ -0,0 +1,98 @@ +CREATE TABLE IF NOT EXISTS `wired_emulator_settings` ( + `key` varchar(191) NOT NULL, + `value` text NOT NULL, + `comment` text NOT NULL, + PRIMARY KEY (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; + +INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`) +SELECT 'wired.engine.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.enabled' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.' +UNION ALL +SELECT 'wired.engine.exclusive', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.exclusive' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.' +UNION ALL +SELECT 'wired.engine.maxStepsPerStack', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.maxStepsPerStack' LIMIT 1), '100'), 'Maximum amount of internal processing steps allowed for a single wired stack execution.' +UNION ALL +SELECT 'wired.engine.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.debug' LIMIT 1), '0'), 'Enable verbose debug logging for the new wired engine.' +UNION ALL +SELECT 'wired.custom.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.custom.enabled' LIMIT 1), '0'), 'Enable custom legacy wired behaviour such as user-based cooldown exceptions and compatibility logic.' +UNION ALL +SELECT 'hotel.wired.furni.selection.count', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.furni.selection.count' LIMIT 1), '5'), 'Maximum number of furni that a wired box can store or select.' +UNION ALL +SELECT 'hotel.wired.max_delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.max_delay' LIMIT 1), '20'), 'Maximum delay value accepted by wired effects that support delayed execution.' +UNION ALL +SELECT 'hotel.wired.message.max_length', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.message.max_length' LIMIT 1), '100'), 'Maximum length of text fields used by wired messages and bot text effects.' +UNION ALL +SELECT 'wired.effect.teleport.delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.effect.teleport.delay' LIMIT 1), '500'), 'Delay in milliseconds used by wired teleport movement.' +UNION ALL +SELECT 'wired.place.under', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.place.under' LIMIT 1), '0'), 'Allow placing wired furniture underneath other items when room rules permit it.' +UNION ALL +SELECT 'wired.tick.interval.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.interval.ms' LIMIT 1), '50'), 'Global wired tick interval in milliseconds used by repeaters and other tick-driven wired items.' +UNION ALL +SELECT 'wired.tick.resolution', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.resolution' LIMIT 1), '100'), 'Legacy wired tick resolution value kept for compatibility with older wired timing setups.' +UNION ALL +SELECT 'wired.tick.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.debug' LIMIT 1), '0'), 'Enable verbose logging for the wired tick service.' +UNION ALL +SELECT 'wired.tick.thread.priority', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.thread.priority' LIMIT 1), '6'), 'Java thread priority used by the wired tick service.' +UNION ALL +SELECT 'wired.highscores.displaycount', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.highscores.displaycount' LIMIT 1), '25'), 'Maximum number of wired highscore entries shown to users when a highscore is displayed.' +UNION ALL +SELECT 'wired.abuse.max.recursion.depth', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.recursion.depth' LIMIT 1), '10'), 'Maximum recursive wired depth allowed before execution is stopped.' +UNION ALL +SELECT 'wired.abuse.max.events.per.window', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.events.per.window' LIMIT 1), '100'), 'Maximum amount of identical wired events allowed inside the abuse rate-limit window before a room ban is applied.' +UNION ALL +SELECT 'wired.abuse.rate.limit.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.rate.limit.window.ms' LIMIT 1), '10000'), 'Time window in milliseconds used by the wired abuse rate limiter.' +UNION ALL +SELECT 'wired.abuse.ban.duration.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.ban.duration.ms' LIMIT 1), '600000'), 'Duration in milliseconds of the temporary wired ban after abuse detection.' +UNION ALL +SELECT 'wired.monitor.usage.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.window.ms' LIMIT 1), '1000'), 'Rolling window size in milliseconds used to calculate wired usage in the :wired monitor.' +UNION ALL +SELECT 'wired.monitor.usage.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.limit' LIMIT 1), '1000'), 'Maximum wired usage budget allowed in one monitor window before EXECUTION_CAP is raised.' +UNION ALL +SELECT 'wired.monitor.delayed.events.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.delayed.events.limit' LIMIT 1), '100'), 'Maximum number of delayed wired events that can be queued in one room at the same time.' +UNION ALL +SELECT 'wired.monitor.overload.average.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.average.ms' LIMIT 1), '50'), 'Average execution time threshold in milliseconds that starts overload tracking.' +UNION ALL +SELECT 'wired.monitor.overload.peak.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.peak.ms' LIMIT 1), '150'), 'Peak single execution time threshold in milliseconds that starts overload tracking.' +UNION ALL +SELECT 'wired.monitor.overload.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.consecutive.windows' LIMIT 1), '2'), 'Number of consecutive overloaded monitor windows required before logging EXECUTOR_OVERLOAD.' +UNION ALL +SELECT 'wired.monitor.heavy.usage.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.usage.percent' LIMIT 1), '70'), 'Usage percentage threshold that contributes to marking a room as heavy in the :wired monitor.' +UNION ALL +SELECT 'wired.monitor.heavy.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.consecutive.windows' LIMIT 1), '5'), 'Number of consecutive windows above the heavy usage threshold required before the room is marked as heavy.' +UNION ALL +SELECT 'wired.monitor.heavy.delayed.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.delayed.percent' LIMIT 1), '60'), 'Delayed queue percentage threshold that also contributes to the heavy-room calculation.' +ON DUPLICATE KEY UPDATE + `value` = VALUES(`value`), + `comment` = VALUES(`comment`); + +DELETE FROM `emulator_settings` +WHERE `key` IN ( + 'wired.engine.enabled', + 'wired.engine.exclusive', + 'wired.engine.maxStepsPerStack', + 'wired.engine.debug', + 'wired.custom.enabled', + 'hotel.wired.furni.selection.count', + 'hotel.wired.max_delay', + 'hotel.wired.message.max_length', + 'wired.effect.teleport.delay', + 'wired.place.under', + 'wired.tick.interval.ms', + 'wired.tick.resolution', + 'wired.tick.debug', + 'wired.tick.thread.priority', + 'wired.highscores.displaycount', + 'wired.abuse.max.recursion.depth', + 'wired.abuse.max.events.per.window', + 'wired.abuse.rate.limit.window.ms', + 'wired.abuse.ban.duration.ms', + 'wired.monitor.usage.window.ms', + 'wired.monitor.usage.limit', + 'wired.monitor.delayed.events.limit', + 'wired.monitor.overload.average.ms', + 'wired.monitor.overload.peak.ms', + 'wired.monitor.overload.consecutive.windows', + 'wired.monitor.heavy.usage.percent', + 'wired.monitor.heavy.consecutive.windows', + 'wired.monitor.heavy.delayed.percent' +); diff --git a/Database Updates/003_add_comment_column_to_emulator_settings.sql b/Database Updates/003_add_comment_column_to_emulator_settings.sql new file mode 100644 index 00000000..63e70a94 --- /dev/null +++ b/Database Updates/003_add_comment_column_to_emulator_settings.sql @@ -0,0 +1,332 @@ +ALTER TABLE `emulator_settings` + ADD COLUMN IF NOT EXISTS `comment` text NOT NULL AFTER `value`; + +UPDATE `emulator_settings` SET `comment` = 'Characters allowed when users choose or change a username.' WHERE `key` = 'allowed.username.characters'; +UPDATE `emulator_settings` SET `comment` = 'Cooldown in milliseconds used by the Apollyon-specific behaviour or command flow.' WHERE `key` = 'apollyon.cooldown.amount'; +UPDATE `emulator_settings` SET `comment` = 'Asset URL used by the BaseJump or FastFood game client.' WHERE `key` = 'basejump.assets.url'; +UPDATE `emulator_settings` SET `comment` = 'SWF URL used to launch the BaseJump or FastFood game client.' WHERE `key` = 'basejump.url'; +UPDATE `emulator_settings` SET `comment` = 'Date format used by visitor bots when they print timestamps.' WHERE `key` = 'bots.visitor.dateformat'; +UPDATE `emulator_settings` SET `comment` = 'Master switch for bubble alert notifications.' WHERE `key` = 'bubblealerts.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Enable bubble alerts when friends come online.' WHERE `key` = 'bubblealerts.notif_friendonline.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Image template used when showing friend-online bubble alerts.' WHERE `key` = 'bubblealerts.notif_friendonline.image'; +UPDATE `emulator_settings` SET `comment` = 'Use the configured figure image inside friend-online bubble alerts.' WHERE `key` = 'bubblealerts.notif_friendonline.useimage'; +UPDATE `emulator_settings` SET `comment` = 'Show bubble alerts for marketplace notifications.' WHERE `key` = 'bubblealerts.notif_marketplace.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Show bubble alerts for limited-item purchases.' WHERE `key` = 'bubblealerts.notif_purchase.limited'; +UPDATE `emulator_settings` SET `comment` = 'Allow bots to be included in room bundles or package rewards.' WHERE `key` = 'bundle.bots.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Allow pets to be included in room bundles or package rewards.' WHERE `key` = 'bundle.pets.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Enable the GET callback used to report version to external services.' WHERE `key` = 'callback.get.version'; +UPDATE `emulator_settings` SET `comment` = 'Enable the POST callback used to report errors to external services.' WHERE `key` = 'callback.post.errors'; +UPDATE `emulator_settings` SET `comment` = 'Enable the POST callback used to report statistics to external services.' WHERE `key` = 'callback.post.statistics'; +UPDATE `emulator_settings` SET `comment` = 'Enable the in-room camera feature.' WHERE `key` = 'camera.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Extradata template written into camera photo items when they are created.' WHERE `key` = 'camera.extradata'; +UPDATE `emulator_settings` SET `comment` = 'Base item ID used by the generated camera photo furniture.' WHERE `key` = 'camera.item_id'; +UPDATE `emulator_settings` SET `comment` = 'Credit price charged when taking a camera photo.' WHERE `key` = 'camera.price.credits'; +UPDATE `emulator_settings` SET `comment` = 'Amount of activity points charged when taking a camera photo.' WHERE `key` = 'camera.price.points'; +UPDATE `emulator_settings` SET `comment` = 'Amount of activity points charged when publishing a camera photo.' WHERE `key` = 'camera.price.points.publish'; +UPDATE `emulator_settings` SET `comment` = 'Activity point type used for the camera publish cost.' WHERE `key` = 'camera.price.points.publish.type'; +UPDATE `emulator_settings` SET `comment` = 'Activity point type used for the camera capture cost.' WHERE `key` = 'camera.price.points.type'; +UPDATE `emulator_settings` SET `comment` = 'Delay in seconds before a published camera photo becomes available.' WHERE `key` = 'camera.publish.delay'; +UPDATE `emulator_settings` SET `comment` = 'Base URL where camera images are published.' WHERE `key` = 'camera.url'; +UPDATE `emulator_settings` SET `comment` = 'Force HTTPS when generating camera image URLs.' WHERE `key` = 'camera.use.https'; +UPDATE `emulator_settings` SET `comment` = 'Require HC or VIP status before users can create a guild.' WHERE `key` = 'catalog.guild.hc_required'; +UPDATE `emulator_settings` SET `comment` = 'Credit cost required to create a guild.' WHERE `key` = 'catalog.guild.price'; +UPDATE `emulator_settings` SET `comment` = 'Layout or image ID used when a limited page is sold out.' WHERE `key` = 'catalog.ltd.page.soldout'; +UPDATE `emulator_settings` SET `comment` = 'Randomize the order or selection of limited catalog items.' WHERE `key` = 'catalog.ltd.random'; +UPDATE `emulator_settings` SET `comment` = 'Catalog page ID used for VIP gift redemption.' WHERE `key` = 'catalog.page.vipgifts'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated list of chat color IDs blocked for the chatcolor command.' WHERE `key` = 'commands.cmd_chatcolor.banned_numbers'; +UPDATE `emulator_settings` SET `comment` = 'Minimum permission rank required to use the staffonline command.' WHERE `key` = 'commands.cmd_staffonline.min_rank'; +UPDATE `emulator_settings` SET `comment` = 'Use the legacy command plugin loading style.' WHERE `key` = 'commands.plugins.oldstyle'; +UPDATE `emulator_settings` SET `comment` = 'Controls the emulator console mode or console output style.' WHERE `key` = 'console.mode'; +UPDATE `emulator_settings` SET `comment` = 'Enable custom item stacking behaviour outside the default stacking rules.' WHERE `key` = 'custom.stacking.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Maximum batch or partition size used by partitioned database operations.' WHERE `key` = 'db.max.partition.size'; +UPDATE `emulator_settings` SET `comment` = 'Minimum batch or partition size used by partitioned database operations.' WHERE `key` = 'db.min.partition.size'; +UPDATE `emulator_settings` SET `comment` = 'Maximum size of the database connection pool.' WHERE `key` = 'db.pool.maxsize'; +UPDATE `emulator_settings` SET `comment` = 'Minimum number of open connections kept in the database pool.' WHERE `key` = 'db.pool.minsize'; +UPDATE `emulator_settings` SET `comment` = 'Enable general emulator debug mode.' WHERE `key` = 'debug.mode'; +UPDATE `emulator_settings` SET `comment` = 'Show internal debug error messages.' WHERE `key` = 'debug.show.errors'; +UPDATE `emulator_settings` SET `comment` = 'Show packet headers in debug logs.' WHERE `key` = 'debug.show.headers'; +UPDATE `emulator_settings` SET `comment` = 'Print packet-level debug output.' WHERE `key` = 'debug.show.packets'; +UPDATE `emulator_settings` SET `comment` = 'Print debug output for undefined incoming or outgoing packets.' WHERE `key` = 'debug.show.packets.undefined'; +UPDATE `emulator_settings` SET `comment` = 'Log SQL exceptions to the console.' WHERE `key` = 'debug.show.sql.exception'; +UPDATE `emulator_settings` SET `comment` = 'Show user-related debug messages.' WHERE `key` = 'debug.show.users'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated discount thresholds used for extra batch bonuses.' WHERE `key` = 'discount.additional.thresholds'; +UPDATE `emulator_settings` SET `comment` = 'Number of free items granted inside one discount batch.' WHERE `key` = 'discount.batch.free.items'; +UPDATE `emulator_settings` SET `comment` = 'Number of items required for one discount batch.' WHERE `key` = 'discount.batch.size'; +UPDATE `emulator_settings` SET `comment` = 'Minimum number of discount batches required before the bonus logic applies.' WHERE `key` = 'discount.bonus.min.discounts'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of catalog items that can participate in one discount batch.' WHERE `key` = 'discount.max.allowed.items'; +UPDATE `emulator_settings` SET `comment` = 'Enable or disable the feature controlled by `easter_eggs.enabled`.' WHERE `key` = 'easter_eggs.enabled'; +UPDATE `emulator_settings` SET `comment` = 'RSA private exponent used by the encryption layer.' WHERE `key` = 'enc.d'; +UPDATE `emulator_settings` SET `comment` = 'RSA public exponent used by the encryption layer.' WHERE `key` = 'enc.e'; +UPDATE `emulator_settings` SET `comment` = 'Enable RSA encryption support for the socket handshake.' WHERE `key` = 'enc.enabled'; +UPDATE `emulator_settings` SET `comment` = 'RSA modulus used by the encryption layer.' WHERE `key` = 'enc.n'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated effect IDs used by the kill command for the killer.' WHERE `key` = 'essentials.cmd_kill.effect.killer'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated effect IDs used by the kill command for the victim.' WHERE `key` = 'essentials.cmd_kill.effect.victim'; +UPDATE `emulator_settings` SET `comment` = 'Allow users with room rights to bypass the normal flood protection.' WHERE `key` = 'flood.with.rights'; +UPDATE `emulator_settings` SET `comment` = 'Enable FTP uploads for generated assets.' WHERE `key` = 'ftp.enabled'; +UPDATE `emulator_settings` SET `comment` = 'FTP host used for asset uploads.' WHERE `key` = 'ftp.host'; +UPDATE `emulator_settings` SET `comment` = 'FTP password used for asset uploads.' WHERE `key` = 'ftp.password'; +UPDATE `emulator_settings` SET `comment` = 'FTP username used for asset uploads.' WHERE `key` = 'ftp.user'; +UPDATE `emulator_settings` SET `comment` = 'Maximum tile distance at which talking furniture can react to nearby speech.' WHERE `key` = 'furniture.talking.range'; +UPDATE `emulator_settings` SET `comment` = 'API key used by the FastFood or BaseJump integration.' WHERE `key` = 'gamecenter.fastfood.apiKey'; +UPDATE `emulator_settings` SET `comment` = 'Asset base URL used by the FastFood or BaseJump game client.' WHERE `key` = 'gamecenter.fastfood.assets'; +UPDATE `emulator_settings` SET `comment` = 'Background color used by the FastFood launcher UI.' WHERE `key` = 'gamecenter.fastfood.background.color'; +UPDATE `emulator_settings` SET `comment` = 'Enable the FastFood or BaseJump gamecenter integration.' WHERE `key` = 'gamecenter.fastfood.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Text color used by the FastFood launcher UI.' WHERE `key` = 'gamecenter.fastfood.text.color'; +UPDATE `emulator_settings` SET `comment` = 'Theme name used by the FastFood launcher.' WHERE `key` = 'gamecenter.fastfood.theme'; +UPDATE `emulator_settings` SET `comment` = 'Background image used for the SnowWar Arctic map.' WHERE `key` = 'gamecenter.snowwar.artic.bg'; +UPDATE `emulator_settings` SET `comment` = 'Asset base URL used by the SnowWar game client.' WHERE `key` = 'gamecenter.snowwar.assets'; +UPDATE `emulator_settings` SET `comment` = 'Background image used for the SnowWar Dragon Cave map.' WHERE `key` = 'gamecenter.snowwar.dragoncave.bg'; +UPDATE `emulator_settings` SET `comment` = 'Enable the SnowWar gamecenter integration.' WHERE `key` = 'gamecenter.snowwar.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Background image used for the SnowWar Fight Night map.' WHERE `key` = 'gamecenter.snowwar.fightnight.bg'; +UPDATE `emulator_settings` SET `comment` = 'Background color used by the SnowWar launcher UI.' WHERE `key` = 'gamecenter.snowwar.game.background.color'; +UPDATE `emulator_settings` SET `comment` = 'Countdown in seconds before a SnowWar round starts.' WHERE `key` = 'gamecenter.snowwar.game.start.time'; +UPDATE `emulator_settings` SET `comment` = 'Text color used by the SnowWar launcher UI.' WHERE `key` = 'gamecenter.snowwar.game.text.color'; +UPDATE `emulator_settings` SET `comment` = 'Minimum number of players required to start SnowWar.' WHERE `key` = 'gamecenter.snowwar.players.min'; +UPDATE `emulator_settings` SET `comment` = 'Room ID used as the SnowWar lobby or host room.' WHERE `key` = 'gamecenter.snowwar.room.id'; +UPDATE `emulator_settings` SET `comment` = 'Remote figuredata URL used when the hotel loads avatar figure definitions.' WHERE `key` = 'gamedata.figuredata.url'; +UPDATE `emulator_settings` SET `comment` = 'Time in seconds that guardians have to accept a case.' WHERE `key` = 'guardians.accept.timer'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of guardians that can be assigned to one case.' WHERE `key` = 'guardians.maximum.guardians.total'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of times an unanswered guardian case can be resent.' WHERE `key` = 'guardians.maximum.resends'; +UPDATE `emulator_settings` SET `comment` = 'Minimum number of guardian votes required to resolve a case.' WHERE `key` = 'guardians.minimum.votes'; +UPDATE `emulator_settings` SET `comment` = 'Cooldown in seconds before the same user can open a new guardian report.' WHERE `key` = 'guardians.reporting.cooldown'; +UPDATE `emulator_settings` SET `comment` = 'Use the legacy generic alert window style.' WHERE `key` = 'hotel.alert.oldstyle'; +UPDATE `emulator_settings` SET `comment` = 'Allow users to ignore staff accounts.' WHERE `key` = 'hotel.allow.ignore.staffs'; +UPDATE `emulator_settings` SET `comment` = 'Amount of credits granted on each automatic payout.' WHERE `key` = 'hotel.auto.credits.amount'; +UPDATE `emulator_settings` SET `comment` = 'Enable automatic credits payouts.' WHERE `key` = 'hotel.auto.credits.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Multiplier applied to automatic credits payouts for HC users.' WHERE `key` = 'hotel.auto.credits.hc_modifier'; +UPDATE `emulator_settings` SET `comment` = 'Skip users staying in hotel view when giving automatic credits payouts.' WHERE `key` = 'hotel.auto.credits.ignore.hotelview'; +UPDATE `emulator_settings` SET `comment` = 'Skip idle users when giving automatic credits payouts.' WHERE `key` = 'hotel.auto.credits.ignore.idled'; +UPDATE `emulator_settings` SET `comment` = 'Interval in seconds between automatic credits payouts.' WHERE `key` = 'hotel.auto.credits.interval'; +UPDATE `emulator_settings` SET `comment` = 'Enable automatic gotwpoints payouts.' WHERE `key` = 'hotel.auto.gotwpoints.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Multiplier applied to automatic gotwpoints payouts for HC users.' WHERE `key` = 'hotel.auto.gotwpoints.hc_modifier'; +UPDATE `emulator_settings` SET `comment` = 'Skip users staying in hotel view when giving automatic gotwpoints payouts.' WHERE `key` = 'hotel.auto.gotwpoints.ignore.hotelview'; +UPDATE `emulator_settings` SET `comment` = 'Skip idle users when giving automatic gotwpoints payouts.' WHERE `key` = 'hotel.auto.gotwpoints.ignore.idled'; +UPDATE `emulator_settings` SET `comment` = 'Interval in seconds between automatic gotwpoints payouts.' WHERE `key` = 'hotel.auto.gotwpoints.interval'; +UPDATE `emulator_settings` SET `comment` = 'Internal currency name used by the automatic gotwpoints payout.' WHERE `key` = 'hotel.auto.gotwpoints.name'; +UPDATE `emulator_settings` SET `comment` = 'Currency type ID used by the automatic gotwpoints payout.' WHERE `key` = 'hotel.auto.gotwpoints.type'; +UPDATE `emulator_settings` SET `comment` = 'Amount of pixels granted on each automatic payout.' WHERE `key` = 'hotel.auto.pixels.amount'; +UPDATE `emulator_settings` SET `comment` = 'Enable automatic pixels payouts.' WHERE `key` = 'hotel.auto.pixels.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Multiplier applied to automatic pixels payouts for HC users.' WHERE `key` = 'hotel.auto.pixels.hc_modifier'; +UPDATE `emulator_settings` SET `comment` = 'Skip users staying in hotel view when giving automatic pixels payouts.' WHERE `key` = 'hotel.auto.pixels.ignore.hotelview'; +UPDATE `emulator_settings` SET `comment` = 'Skip idle users when giving automatic pixels payouts.' WHERE `key` = 'hotel.auto.pixels.ignore.idled'; +UPDATE `emulator_settings` SET `comment` = 'Interval in seconds between automatic pixels payouts.' WHERE `key` = 'hotel.auto.pixels.interval'; +UPDATE `emulator_settings` SET `comment` = 'Amount of points granted on each automatic payout.' WHERE `key` = 'hotel.auto.points.amount'; +UPDATE `emulator_settings` SET `comment` = 'Enable automatic points payouts.' WHERE `key` = 'hotel.auto.points.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Multiplier applied to automatic points payouts for HC users.' WHERE `key` = 'hotel.auto.points.hc_modifier'; +UPDATE `emulator_settings` SET `comment` = 'Skip users staying in hotel view when giving automatic points payouts.' WHERE `key` = 'hotel.auto.points.ignore.hotelview'; +UPDATE `emulator_settings` SET `comment` = 'Skip idle users when giving automatic points payouts.' WHERE `key` = 'hotel.auto.points.ignore.idled'; +UPDATE `emulator_settings` SET `comment` = 'Interval in seconds between automatic points payouts.' WHERE `key` = 'hotel.auto.points.interval'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.banzai.points.tile.fill`.' WHERE `key` = 'hotel.banzai.points.tile.fill'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.banzai.points.tile.lock`.' WHERE `key` = 'hotel.banzai.points.tile.lock'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.banzai.points.tile.steal`.' WHERE `key` = 'hotel.banzai.points.tile.steal'; +UPDATE `emulator_settings` SET `comment` = 'Maximum tile distance from which a butler bot accepts commands.' WHERE `key` = 'hotel.bot.butler.commanddistance'; +UPDATE `emulator_settings` SET `comment` = 'Maximum tile distance from which a butler bot can serve requests.' WHERE `key` = 'hotel.bot.butler.servedistance'; +UPDATE `emulator_settings` SET `comment` = 'Minimum number of seconds between bot chat lines.' WHERE `key` = 'hotel.bot.chat.minimum.interval'; +UPDATE `emulator_settings` SET `comment` = 'Maximum bot chat delay allowed when configuring scripted speech.' WHERE `key` = 'hotel.bot.max.chatdelay'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of characters allowed in bot chat lines.' WHERE `key` = 'hotel.bot.max.chatlength'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of characters allowed in bot names.' WHERE `key` = 'hotel.bot.max.namelength'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of bots allowed in one inventory.' WHERE `key` = 'hotel.bots.max.inventory'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of bots allowed in one room.' WHERE `key` = 'hotel.bots.max.room'; +UPDATE `emulator_settings` SET `comment` = 'Default calendar campaign name or identifier.' WHERE `key` = 'hotel.calendar.default'; +UPDATE `emulator_settings` SET `comment` = 'Enable the hotel calendar feature.' WHERE `key` = 'hotel.calendar.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Multiplier applied to calendar pixel rewards for HC users.' WHERE `key` = 'hotel.calendar.pixels.hc_modifier'; +UPDATE `emulator_settings` SET `comment` = 'Unix timestamp used as the calendar start date.' WHERE `key` = 'hotel.calendar.starttimestamp'; +UPDATE `emulator_settings` SET `comment` = 'Number of discount slots or discount batches shown by the catalog.' WHERE `key` = 'hotel.catalog.discounts.amount'; +UPDATE `emulator_settings` SET `comment` = 'Respect catalog item order numbers when rendering pages.' WHERE `key` = 'hotel.catalog.items.display.ordernum'; +UPDATE `emulator_settings` SET `comment` = 'Enable daily purchase limits for limited catalog items.' WHERE `key` = 'hotel.catalog.ltd.limit.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Cooldown in seconds between catalog purchases.' WHERE `key` = 'hotel.catalog.purchase.cooldown'; +UPDATE `emulator_settings` SET `comment` = 'Enable the catalog recycler feature.' WHERE `key` = 'hotel.catalog.recycler.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of characters allowed in one public chat message.' WHERE `key` = 'hotel.chat.max.length'; +UPDATE `emulator_settings` SET `comment` = 'Daily amount of respect points available for users.' WHERE `key` = 'hotel.daily.respect'; +UPDATE `emulator_settings` SET `comment` = 'Daily amount of pet respect points available for users.' WHERE `key` = 'hotel.daily.respect.pets'; +UPDATE `emulator_settings` SET `comment` = 'Enable or disable the feature controlled by `hotel.ecotron.enabled`.' WHERE `key` = 'hotel.ecotron.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.ecotron.rarity.chance.1`.' WHERE `key` = 'hotel.ecotron.rarity.chance.1'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.ecotron.rarity.chance.2`.' WHERE `key` = 'hotel.ecotron.rarity.chance.2'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.ecotron.rarity.chance.3`.' WHERE `key` = 'hotel.ecotron.rarity.chance.3'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.ecotron.rarity.chance.4`.' WHERE `key` = 'hotel.ecotron.rarity.chance.4'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.ecotron.rarity.chance.5`.' WHERE `key` = 'hotel.ecotron.rarity.chance.5'; +UPDATE `emulator_settings` SET `comment` = 'Mute duration in seconds applied by the hotel flood protection.' WHERE `key` = 'hotel.flood.mute.time'; +UPDATE `emulator_settings` SET `comment` = 'Maximum total floorplan area allowed for custom rooms.' WHERE `key` = 'hotel.floorplan.max.totalarea'; +UPDATE `emulator_settings` SET `comment` = 'Maximum floorplan width or length allowed for custom rooms.' WHERE `key` = 'hotel.floorplan.max.widthlength'; +UPDATE `emulator_settings` SET `comment` = 'Number of explosion boosts lost when a player gets frozen.' WHERE `key` = 'hotel.freeze.onfreeze.loose.explosionboost'; +UPDATE `emulator_settings` SET `comment` = 'Number of snowballs lost when a player gets frozen.' WHERE `key` = 'hotel.freeze.onfreeze.loose.snowballs'; +UPDATE `emulator_settings` SET `comment` = 'Time in seconds a player remains frozen.' WHERE `key` = 'hotel.freeze.onfreeze.time.frozen'; +UPDATE `emulator_settings` SET `comment` = 'Score awarded for blocking tiles in Freeze.' WHERE `key` = 'hotel.freeze.points.block'; +UPDATE `emulator_settings` SET `comment` = 'Score awarded for using Freeze effects or power-up actions.' WHERE `key` = 'hotel.freeze.points.effect'; +UPDATE `emulator_settings` SET `comment` = 'Score awarded for freezing another player in Freeze.' WHERE `key` = 'hotel.freeze.points.freeze'; +UPDATE `emulator_settings` SET `comment` = 'Chance for Freeze power-ups to spawn.' WHERE `key` = 'hotel.freeze.powerup.chance'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of extra lives granted by a Freeze power-up.' WHERE `key` = 'hotel.freeze.powerup.max.lives'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of extra snowballs granted by a Freeze power-up.' WHERE `key` = 'hotel.freeze.powerup.max.snowballs'; +UPDATE `emulator_settings` SET `comment` = 'Allow Freeze protection power-ups to stack.' WHERE `key` = 'hotel.freeze.powerup.protection.stack'; +UPDATE `emulator_settings` SET `comment` = 'Protection time in seconds after receiving a Freeze protection power-up.' WHERE `key` = 'hotel.freeze.powerup.protection.time'; +UPDATE `emulator_settings` SET `comment` = 'Default friend category ID assigned to new friends.' WHERE `key` = 'hotel.friendcategory'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.furni.gym.achievement.olympics_c16_crosstrainer`.' WHERE `key` = 'hotel.furni.gym.achievement.olympics_c16_crosstrainer'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.furni.gym.achievement.olympics_c16_trampoline`.' WHERE `key` = 'hotel.furni.gym.achievement.olympics_c16_trampoline'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.furni.gym.achievement.olympics_c16_treadmill`.' WHERE `key` = 'hotel.furni.gym.achievement.olympics_c16_treadmill'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.furni.gym.forcerot.olympics_c16_crosstrainer`.' WHERE `key` = 'hotel.furni.gym.forcerot.olympics_c16_crosstrainer'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.furni.gym.forcerot.olympics_c16_trampoline`.' WHERE `key` = 'hotel.furni.gym.forcerot.olympics_c16_trampoline'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.furni.gym.forcerot.olympics_c16_treadmill`.' WHERE `key` = 'hotel.furni.gym.forcerot.olympics_c16_treadmill'; +UPDATE `emulator_settings` SET `comment` = 'Comma-separated list of gift box type IDs allowed in the catalog.' WHERE `key` = 'hotel.gifts.box_types'; +UPDATE `emulator_settings` SET `comment` = 'Maximum message length allowed on gift notes.' WHERE `key` = 'hotel.gifts.length.max'; +UPDATE `emulator_settings` SET `comment` = 'Comma-separated list of ribbon type IDs allowed in the catalog.' WHERE `key` = 'hotel.gifts.ribbon_types'; +UPDATE `emulator_settings` SET `comment` = 'Credit price used by special gift boxes.' WHERE `key` = 'hotel.gifts.special.price'; +UPDATE `emulator_settings` SET `comment` = 'Room ID used as the default home room for new users.' WHERE `key` = 'hotel.home.room'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of items allowed in one inventory.' WHERE `key` = 'hotel.inventory.max.items'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.item.trap.hween14_rare2`.' WHERE `key` = 'hotel.item.trap.hween14_rare2'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.item.trap.hween_c17_handstrap`.' WHERE `key` = 'hotel.item.trap.hween_c17_handstrap'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.item.trap.hween_c17_spiketrap`.' WHERE `key` = 'hotel.item.trap.hween_c17_spiketrap'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `hotel.item.trap.pirate_sandtrap`.' WHERE `key` = 'hotel.item.trap.pirate_sandtrap'; +UPDATE `emulator_settings` SET `comment` = 'Track limit used by large jukebox furniture.' WHERE `key` = 'hotel.jukebox.limit.large'; +UPDATE `emulator_settings` SET `comment` = 'Track limit used by normal jukebox furniture.' WHERE `key` = 'hotel.jukebox.limit.normal'; +UPDATE `emulator_settings` SET `comment` = 'Enable logging for chat.' WHERE `key` = 'hotel.log.chat'; +UPDATE `emulator_settings` SET `comment` = 'Enable logging for chat private.' WHERE `key` = 'hotel.log.chat.private'; +UPDATE `emulator_settings` SET `comment` = 'Enable logging for room enter.' WHERE `key` = 'hotel.log.room.enter'; +UPDATE `emulator_settings` SET `comment` = 'Enable logging for trades.' WHERE `key` = 'hotel.log.trades'; +UPDATE `emulator_settings` SET `comment` = 'Currency type used for marketplace prices and taxes.' WHERE `key` = 'hotel.marketplace.currency'; +UPDATE `emulator_settings` SET `comment` = 'Enable or disable the feature controlled by `hotel.marketplace.enabled`.' WHERE `key` = 'hotel.marketplace.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of bots allowed in one room.' WHERE `key` = 'hotel.max.bots.room'; +UPDATE `emulator_settings` SET `comment` = 'Maximum amount of duckets a user can hold.' WHERE `key` = 'hotel.max.duckets'; +UPDATE `emulator_settings` SET `comment` = 'Enable or disable the feature controlled by `hotel.messenger.offline.messaging.enabled`.' WHERE `key` = 'hotel.messenger.offline.messaging.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of results returned by messenger user searches.' WHERE `key` = 'hotel.messenger.search.maxresults'; +UPDATE `emulator_settings` SET `comment` = 'Public hotel name shown across the client and outgoing messages.' WHERE `key` = 'hotel.name'; +UPDATE `emulator_settings` SET `comment` = 'Enable navigator room previews or camera mode.' WHERE `key` = 'hotel.navigator.camera'; +UPDATE `emulator_settings` SET `comment` = 'Default owner name displayed by the navigator.' WHERE `key` = 'hotel.navigator.owner'; +UPDATE `emulator_settings` SET `comment` = 'Number of rooms shown in the popular rooms list.' WHERE `key` = 'hotel.navigator.popular.amount'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of rooms shown per popular category.' WHERE `key` = 'hotel.navigator.popular.category.maxresults'; +UPDATE `emulator_settings` SET `comment` = 'List type used for the popular rooms tab.' WHERE `key` = 'hotel.navigator.popular.listtype'; +UPDATE `emulator_settings` SET `comment` = 'Include public rooms inside the popular rooms tab.' WHERE `key` = 'hotel.navigator.populartab.publics'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of results returned by navigator searches.' WHERE `key` = 'hotel.navigator.search.maxresults'; +UPDATE `emulator_settings` SET `comment` = 'Respect order numbers when sorting navigator results.' WHERE `key` = 'hotel.navigator.sort.ordernum'; +UPDATE `emulator_settings` SET `comment` = 'Category ID used for the staff picks tab.' WHERE `key` = 'hotel.navigator.staffpicks.categoryid'; +UPDATE `emulator_settings` SET `comment` = 'Enable the NUX gift flow for new users.' WHERE `key` = 'hotel.nux.gifts.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of pets allowed in one inventory.' WHERE `key` = 'hotel.pets.max.inventory'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of pets allowed in one room.' WHERE `key` = 'hotel.pets.max.room'; +UPDATE `emulator_settings` SET `comment` = 'Maximum pet name length.' WHERE `key` = 'hotel.pets.name.length.max'; +UPDATE `emulator_settings` SET `comment` = 'Minimum pet name length.' WHERE `key` = 'hotel.pets.name.length.min'; +UPDATE `emulator_settings` SET `comment` = 'Generic player label used by text templates and client messages.' WHERE `key` = 'hotel.player.name'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of the same limited item a user can buy per day.' WHERE `key` = 'hotel.purchase.ltd.limit.daily.item'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of limited items a user can buy per day across all limited sales.' WHERE `key` = 'hotel.purchase.ltd.limit.daily.total'; +UPDATE `emulator_settings` SET `comment` = 'Cooldown in seconds before daily counters such as respect are refilled.' WHERE `key` = 'hotel.refill.daily'; +UPDATE `emulator_settings` SET `comment` = 'Maximum roller delay or speed value accepted by roller furniture.' WHERE `key` = 'hotel.rollers.speed.maximum'; +UPDATE `emulator_settings` SET `comment` = 'Enable room-entry logs.' WHERE `key` = 'hotel.room.enter.logs'; +UPDATE `emulator_settings` SET `comment` = 'Validate custom floorplans before rooms are saved.' WHERE `key` = 'hotel.room.floorplan.check.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Maximum amount of furniture allowed in one room.' WHERE `key` = 'hotel.room.furni.max'; +UPDATE `emulator_settings` SET `comment` = 'Room ID used as the newbie lobby.' WHERE `key` = 'hotel.room.nooblobby'; +UPDATE `emulator_settings` SET `comment` = 'Kick users who stand on public room door tiles.' WHERE `key` = 'hotel.room.public.doortile.kick'; +UPDATE `emulator_settings` SET `comment` = 'Allow rollers to ignore normal placement rules.' WHERE `key` = 'hotel.room.rollers.norules'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of avatars that rollers can move at once.' WHERE `key` = 'hotel.room.rollers.roll_avatars.max'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of sticky notes allowed in one room.' WHERE `key` = 'hotel.room.stickies.max'; +UPDATE `emulator_settings` SET `comment` = 'Prefix template written by sticky pole furniture.' WHERE `key` = 'hotel.room.stickypole.prefix'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated staff room tags.' WHERE `key` = 'hotel.room.tags.staff'; +UPDATE `emulator_settings` SET `comment` = 'Allow empty rooms to switch into the idle state automatically.' WHERE `key` = 'hotel.rooms.auto.idle'; +UPDATE `emulator_settings` SET `comment` = 'Enable decoration-hosting features for rooms.' WHERE `key` = 'hotel.rooms.deco_hosting'; +UPDATE `emulator_settings` SET `comment` = 'Time in seconds before temporary hand items are cleared.' WHERE `key` = 'hotel.rooms.handitem.time'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of favorite rooms allowed per user.' WHERE `key` = 'hotel.rooms.max.favorite'; +UPDATE `emulator_settings` SET `comment` = 'Idle cycle count before a room user is marked idle.' WHERE `key` = 'hotel.roomuser.idle.cycles'; +UPDATE `emulator_settings` SET `comment` = 'Idle cycle count before a room user is kicked for idling.' WHERE `key` = 'hotel.roomuser.idle.cycles.kick'; +UPDATE `emulator_settings` SET `comment` = 'Ignore the wired idle status when checking the room idle rule.' WHERE `key` = 'hotel.roomuser.idle.not_dancing.ignore.wired_idle'; +UPDATE `emulator_settings` SET `comment` = 'Enable the sanctions system.' WHERE `key` = 'hotel.sanctions.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Modifier used by the shop discount calculation.' WHERE `key` = 'hotel.shop.discount.modifier'; +UPDATE `emulator_settings` SET `comment` = 'Enable the talent track feature.' WHERE `key` = 'hotel.talenttrack.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Offer ID requested when the client asks for a targeted offer.' WHERE `key` = 'hotel.targetoffer.id'; +UPDATE `emulator_settings` SET `comment` = 'Allow users to use teleports inside locked rooms when they otherwise qualify.' WHERE `key` = 'hotel.teleport.locked.allowed'; +UPDATE `emulator_settings` SET `comment` = 'Enable room trading.' WHERE `key` = 'hotel.trading.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Require the trading perk before users may trade.' WHERE `key` = 'hotel.trading.requires.perk'; +UPDATE `emulator_settings` SET `comment` = 'Maximum value used by `hotel.trophies.length.max`.' WHERE `key` = 'hotel.trophies.length.max'; +UPDATE `emulator_settings` SET `comment` = 'Run clothing validation when the related action occurs: onchangelooks.' WHERE `key` = 'hotel.users.clothingvalidation.onchangelooks'; +UPDATE `emulator_settings` SET `comment` = 'Run clothing validation when the related action occurs: onfballgate.' WHERE `key` = 'hotel.users.clothingvalidation.onfballgate'; +UPDATE `emulator_settings` SET `comment` = 'Run clothing validation when the related action occurs: onhcexpired.' WHERE `key` = 'hotel.users.clothingvalidation.onhcexpired'; +UPDATE `emulator_settings` SET `comment` = 'Run clothing validation when the related action occurs: onlogin.' WHERE `key` = 'hotel.users.clothingvalidation.onlogin'; +UPDATE `emulator_settings` SET `comment` = 'Run clothing validation when the related action occurs: onmannequin.' WHERE `key` = 'hotel.users.clothingvalidation.onmannequin'; +UPDATE `emulator_settings` SET `comment` = 'Run clothing validation when the related action occurs: onmimic.' WHERE `key` = 'hotel.users.clothingvalidation.onmimic'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of friends allowed for normal users.' WHERE `key` = 'hotel.users.max.friends'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of friends allowed for HC users.' WHERE `key` = 'hotel.users.max.friends.hc'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of rooms allowed for normal users.' WHERE `key` = 'hotel.users.max.rooms'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of rooms allowed for HC users.' WHERE `key` = 'hotel.users.max.rooms.hc'; +UPDATE `emulator_settings` SET `comment` = 'Enable the limited-countdown hotel-view widget.' WHERE `key` = 'hotel.view.ltdcountdown.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Item ID shown by the limited-countdown widget.' WHERE `key` = 'hotel.view.ltdcountdown.itemid'; +UPDATE `emulator_settings` SET `comment` = 'Item name shown by the limited-countdown widget.' WHERE `key` = 'hotel.view.ltdcountdown.itemname'; +UPDATE `emulator_settings` SET `comment` = 'Catalog page ID linked by the limited-countdown widget.' WHERE `key` = 'hotel.view.ltdcountdown.pageid'; +UPDATE `emulator_settings` SET `comment` = 'Unix timestamp used by the limited-countdown widget.' WHERE `key` = 'hotel.view.ltdcountdown.timestamp'; +UPDATE `emulator_settings` SET `comment` = 'Delay in milliseconds before the welcome alert is shown.' WHERE `key` = 'hotel.welcome.alert.delay'; +UPDATE `emulator_settings` SET `comment` = 'Enable the welcome alert shown after login.' WHERE `key` = 'hotel.welcome.alert.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Message template used by the welcome alert.' WHERE `key` = 'hotel.welcome.alert.message'; +UPDATE `emulator_settings` SET `comment` = 'Use the legacy welcome alert window style.' WHERE `key` = 'hotel.welcome.alert.oldstyle'; +UPDATE `emulator_settings` SET `comment` = 'Mute duration in minutes applied when word-filter automute is triggered.' WHERE `key` = 'hotel.wordfilter.automute'; +UPDATE `emulator_settings` SET `comment` = 'Enable the word filter system.' WHERE `key` = 'hotel.wordfilter.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Apply the word filter to messenger messages.' WHERE `key` = 'hotel.wordfilter.messenger'; +UPDATE `emulator_settings` SET `comment` = 'Normalise text before checking it against the word filter.' WHERE `key` = 'hotel.wordfilter.normalise'; +UPDATE `emulator_settings` SET `comment` = 'Replacement word used when text is censored.' WHERE `key` = 'hotel.wordfilter.replacement'; +UPDATE `emulator_settings` SET `comment` = 'Apply the word filter to room chat.' WHERE `key` = 'hotel.wordfilter.rooms'; +UPDATE `emulator_settings` SET `comment` = 'SQL query used to populate the hotel-view hall of fame panel.' WHERE `key` = 'hotelview.halloffame.query'; +UPDATE `emulator_settings` SET `comment` = 'Amount of activity points awarded by the hotel-view promotion.' WHERE `key` = 'hotelview.promotional.points'; +UPDATE `emulator_settings` SET `comment` = 'Activity point type used by the hotel-view promotional reward.' WHERE `key` = 'hotelview.promotional.points.type'; +UPDATE `emulator_settings` SET `comment` = 'Base item ID used by the hotel-view promotional reward.' WHERE `key` = 'hotelview.promotional.reward.id'; +UPDATE `emulator_settings` SET `comment` = 'Public item name used by the hotel-view promotional reward.' WHERE `key` = 'hotelview.promotional.reward.name'; +UPDATE `emulator_settings` SET `comment` = 'Generate images locally instead of relying on an external imager service.' WHERE `key` = 'imager.internal.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Filesystem path where badge part assets are stored.' WHERE `key` = 'imager.location.badgeparts'; +UPDATE `emulator_settings` SET `comment` = 'Filesystem output path for generated badges.' WHERE `key` = 'imager.location.output.badges'; +UPDATE `emulator_settings` SET `comment` = 'Filesystem output path for saved camera photos.' WHERE `key` = 'imager.location.output.camera'; +UPDATE `emulator_settings` SET `comment` = 'Filesystem output path for generated camera thumbnails.' WHERE `key` = 'imager.location.output.thumbnail'; +UPDATE `emulator_settings` SET `comment` = 'Template URL used to fetch YouTube thumbnails.' WHERE `key` = 'imager.url.youtube'; +UPDATE `emulator_settings` SET `comment` = 'Client asset path used for the basejump gamecenter images.' WHERE `key` = 'images.gamecenter.basejump'; +UPDATE `emulator_settings` SET `comment` = 'Client asset path used for the snowwar gamecenter images.' WHERE `key` = 'images.gamecenter.snowwar'; +UPDATE `emulator_settings` SET `comment` = 'Show the hotel information panel or startup information message.' WHERE `key` = 'info.shown'; +UPDATE `emulator_settings` SET `comment` = 'Prevent invisible users from speaking in rooms.' WHERE `key` = 'invisible.prevent.chat'; +UPDATE `emulator_settings` SET `comment` = 'Number of Netty boss-group threads used by the socket server.' WHERE `key` = 'io.bossgroup.threads'; +UPDATE `emulator_settings` SET `comment` = 'Handle incoming client packets with a multi-threaded pipeline.' WHERE `key` = 'io.client.multithreaded.handler'; +UPDATE `emulator_settings` SET `comment` = 'Number of Netty worker-group threads used by the socket server.' WHERE `key` = 'io.workergroup.threads'; +UPDATE `emulator_settings` SET `comment` = 'Enable extra debug logging in the emulator logger.' WHERE `key` = 'logging.debug'; +UPDATE `emulator_settings` SET `comment` = 'Log packet parsing errors.' WHERE `key` = 'logging.errors.packets'; +UPDATE `emulator_settings` SET `comment` = 'Log runtime exceptions.' WHERE `key` = 'logging.errors.runtime'; +UPDATE `emulator_settings` SET `comment` = 'Log SQL errors.' WHERE `key` = 'logging.errors.sql'; +UPDATE `emulator_settings` SET `comment` = 'Log packet traffic in the standard logger.' WHERE `key` = 'logging.packets'; +UPDATE `emulator_settings` SET `comment` = 'Log undefined packets in the standard logger.' WHERE `key` = 'logging.packets.undefined'; +UPDATE `emulator_settings` SET `comment` = 'Global switch for the marketplace subsystem.' WHERE `key` = 'marketplace.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `monsterplant.seed.item_id`.' WHERE `key` = 'monsterplant.seed.item_id'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `monsterplant.seed_rare.item_id`.' WHERE `key` = 'monsterplant.seed_rare.item_id'; +UPDATE `emulator_settings` SET `comment` = 'Validate moodlight color values before applying them.' WHERE `key` = 'moodlight.color_check.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated navigator event category definitions shown in the events tab.' WHERE `key` = 'navigator.eventcategories'; +UPDATE `emulator_settings` SET `comment` = 'Enable TCP proxy-aware networking behaviour.' WHERE `key` = 'networking.tcp.proxy'; +UPDATE `emulator_settings` SET `comment` = 'Automatically notify staff when a chat report is created.' WHERE `key` = 'notify.staff.chat.auto.report'; +UPDATE `emulator_settings` SET `comment` = 'Base path used by the client to load furniture icon assets.' WHERE `key` = 'path.furniture.icons'; +UPDATE `emulator_settings` SET `comment` = 'Maximum pathfinder execution time in milliseconds before aborting.' WHERE `key` = 'pathfinder.execution_time.milli'; +UPDATE `emulator_settings` SET `comment` = 'Enforce the pathfinder execution time limit.' WHERE `key` = 'pathfinder.max_execution_time.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Allow the pathfinder to walk down falling steps.' WHERE `key` = 'pathfinder.step.allow.falling'; +UPDATE `emulator_settings` SET `comment` = 'Maximum height difference the pathfinder may step onto.' WHERE `key` = 'pathfinder.step.maximum.height'; +UPDATE `emulator_settings` SET `comment` = 'Chat bubble style ID used by the pirate parrot.' WHERE `key` = 'pirate_parrot.message.bubble'; +UPDATE `emulator_settings` SET `comment` = 'Number of predefined messages available to the pirate parrot.' WHERE `key` = 'pirate_parrot.message.count'; +UPDATE `emulator_settings` SET `comment` = 'Maximum number of characters allowed on post-it notes.' WHERE `key` = 'postit.charlimit'; +UPDATE `emulator_settings` SET `comment` = 'Maximum delay allowed in the Pyramids minigame or puzzle timing.' WHERE `key` = 'pyramids.max.delay'; +UPDATE `emulator_settings` SET `comment` = 'Use retro-style home room behaviour in the navigator or onboarding flow.' WHERE `key` = 'retro.style.homeroom'; +UPDATE `emulator_settings` SET `comment` = 'Extra room chat delay applied before users can speak again.' WHERE `key` = 'room.chat.delay'; +UPDATE `emulator_settings` SET `comment` = 'Allow whispering while a user stands inside a mute area.' WHERE `key` = 'room.chat.mutearea.allow_whisper'; +UPDATE `emulator_settings` SET `comment` = 'HTML or text format used for room chat prefixes.' WHERE `key` = 'room.chat.prefix.format'; +UPDATE `emulator_settings` SET `comment` = 'Badge code displayed on promoted rooms.' WHERE `key` = 'room.promotion.badge'; +UPDATE `emulator_settings` SET `comment` = 'Image used by Rosie bubble notifications.' WHERE `key` = 'rosie.bubble.image.url'; +UPDATE `emulator_settings` SET `comment` = 'Currency type used by Rosie when buying a room or room package.' WHERE `key` = 'rosie.buyroom.currency.type'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `runtime.threads`.' WHERE `key` = 'runtime.threads'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.private.chats`.' WHERE `key` = 'save.private.chats'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.room.chats`.' WHERE `key` = 'save.room.chats'; +UPDATE `emulator_settings` SET `comment` = 'Expose moderation tickets to the scripter or automation tooling.' WHERE `key` = 'scripter.modtool.tickets'; +UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for diamonds.' WHERE `key` = 'seasonal.currency.diamond'; +UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for duckets.' WHERE `key` = 'seasonal.currency.ducket'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated display names for seasonal currency types.' WHERE `key` = 'seasonal.currency.names'; +UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for pixels.' WHERE `key` = 'seasonal.currency.pixel'; +UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for shells.' WHERE `key` = 'seasonal.currency.shell'; +UPDATE `emulator_settings` SET `comment` = 'Primary seasonal currency type ID.' WHERE `key` = 'seasonal.primary.type'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated list of currency type IDs treated as seasonal currencies.' WHERE `key` = 'seasonal.types'; +UPDATE `emulator_settings` SET `comment` = 'Achievement code granted for the HC subscription tier.' WHERE `key` = 'subscriptions.hc.achievement'; +UPDATE `emulator_settings` SET `comment` = 'Number of days before expiry when HC discount offers become available.' WHERE `key` = 'subscriptions.hc.discount.days_before_end'; +UPDATE `emulator_settings` SET `comment` = 'Enable discounted HC renewal offers.' WHERE `key` = 'subscriptions.hc.discount.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Reset tracked credits spent when the HC subscription expires.' WHERE `key` = 'subscriptions.hc.payday.creditsspent_reset_on_expire'; +UPDATE `emulator_settings` SET `comment` = 'Currency rewarded by the HC payday system.' WHERE `key` = 'subscriptions.hc.payday.currency'; +UPDATE `emulator_settings` SET `comment` = 'Enable the HC payday reward system.' WHERE `key` = 'subscriptions.hc.payday.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Date interval used between HC payday reward runs.' WHERE `key` = 'subscriptions.hc.payday.interval'; +UPDATE `emulator_settings` SET `comment` = 'Next scheduled execution date for HC payday rewards.' WHERE `key` = 'subscriptions.hc.payday.next_date'; +UPDATE `emulator_settings` SET `comment` = 'Percentage of eligible spending returned by HC payday.' WHERE `key` = 'subscriptions.hc.payday.percentage'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated streak thresholds and rewards for HC payday.' WHERE `key` = 'subscriptions.hc.payday.streak'; +UPDATE `emulator_settings` SET `comment` = 'Enable the subscription background scheduler.' WHERE `key` = 'subscriptions.scheduler.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Interval in minutes between subscription scheduler runs.' WHERE `key` = 'subscriptions.scheduler.interval'; +UPDATE `emulator_settings` SET `comment` = 'Compatibility marker used by the custom team wired implementation. Do not remove.' WHERE `key` = 'team.wired.update.rc-1'; +UPDATE `emulator_settings` SET `comment` = 'API key used by the YouTube integration.' WHERE `key` = 'youtube.apikey'; diff --git a/Database Updates/003_furni_editor.sql b/Database Updates/003_furni_editor.sql new file mode 100644 index 00000000..37bfc80c --- /dev/null +++ b/Database Updates/003_furni_editor.sql @@ -0,0 +1,6 @@ +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES + ('furni.editor.renderer.config.path', '/var/www/Gamedata/config/renderer-config.json'), + ('furni.editor.asset.base.path', '/var/www/Gamedata/furniture/nitro-assets/'); + +ALTER TABLE permissions +ADD COLUMN `acc_catalogfurni` ENUM('0','1') NOT NULL DEFAULT '0' AFTER `acc_catalog_ids`; \ No newline at end of file diff --git a/Database Updates/004_Remove_Youtube_Thumbnail.sql b/Database Updates/004_Remove_Youtube_Thumbnail.sql new file mode 100644 index 00000000..938c079f --- /dev/null +++ b/Database Updates/004_Remove_Youtube_Thumbnail.sql @@ -0,0 +1 @@ +DELETE FROM emulator_settings WHERE (`key` = 'youtube.apikey'); diff --git a/Database Updates/004_normalize_permissions_schema.sql b/Database Updates/004_normalize_permissions_schema.sql new file mode 100644 index 00000000..51582c5c --- /dev/null +++ b/Database Updates/004_normalize_permissions_schema.sql @@ -0,0 +1,499 @@ +-- Normalizes the legacy `permissions` table into: +-- 1. `permission_ranks` -> one row per rank with rank metadata. +-- 2. `permission_definitions` -> one row per permission key with comments and one `rank_` column per rank. +-- +-- This migration keeps the old `permissions` table untouched so the emulator can safely fall back to it. +-- It also cleans up the older experimental normalized objects if they were already created. + +DROP VIEW IF EXISTS `permissions_matrix_view`; +DROP PROCEDURE IF EXISTS `refresh_permissions_matrix_view`; +DROP TABLE IF EXISTS `permission_rank_values`; +DROP TABLE IF EXISTS `permission_nodes`; + +CREATE TABLE IF NOT EXISTS `permission_ranks` ( + `id` int(11) NOT NULL, + `rank_name` varchar(25) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL, + `hidden_rank` tinyint(1) NOT NULL DEFAULT 0, + `badge` varchar(12) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '', + `job_description` varchar(255) NOT NULL DEFAULT 'Here to help', + `staff_color` varchar(8) NOT NULL DEFAULT '#327fa8', + `staff_background` varchar(255) NOT NULL DEFAULT 'staff-bg.png', + `level` int(11) NOT NULL DEFAULT 1, + `room_effect` int(11) NOT NULL DEFAULT 0, + `log_commands` enum('0','1') CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '0', + `prefix` varchar(5) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '', + `prefix_color` varchar(7) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '', + `auto_credits_amount` int(11) DEFAULT 0, + `auto_pixels_amount` int(11) DEFAULT 0, + `auto_gotw_amount` int(11) DEFAULT 0, + `auto_points_amount` int(11) DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE IF NOT EXISTS `permission_definitions` ( + `permission_key` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL, + `max_value` tinyint(3) unsigned NOT NULL DEFAULT 1, + `comment` text NOT NULL, + PRIMARY KEY (`permission_key`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci ROW_FORMAT=DYNAMIC; + +ALTER TABLE `permission_definitions` + DROP COLUMN IF EXISTS `category`, + DROP COLUMN IF EXISTS `value_type`, + DROP COLUMN IF EXISTS `sort_order`; + +INSERT INTO `permission_ranks` ( + `id`, + `rank_name`, + `hidden_rank`, + `badge`, + `job_description`, + `staff_color`, + `staff_background`, + `level`, + `room_effect`, + `log_commands`, + `prefix`, + `prefix_color`, + `auto_credits_amount`, + `auto_pixels_amount`, + `auto_gotw_amount`, + `auto_points_amount` +) +SELECT + `id`, + `rank_name`, + `hidden_rank`, + `badge`, + `job_description`, + `staff_color`, + `staff_background`, + `level`, + `room_effect`, + `log_commands`, + `prefix`, + `prefix_color`, + `auto_credits_amount`, + `auto_pixels_amount`, + `auto_gotw_amount`, + `auto_points_amount` +FROM `permissions` +ON DUPLICATE KEY UPDATE + `rank_name` = VALUES(`rank_name`), + `hidden_rank` = VALUES(`hidden_rank`), + `badge` = VALUES(`badge`), + `job_description` = VALUES(`job_description`), + `staff_color` = VALUES(`staff_color`), + `staff_background` = VALUES(`staff_background`), + `level` = VALUES(`level`), + `room_effect` = VALUES(`room_effect`), + `log_commands` = VALUES(`log_commands`), + `prefix` = VALUES(`prefix`), + `prefix_color` = VALUES(`prefix_color`), + `auto_credits_amount` = VALUES(`auto_credits_amount`), + `auto_pixels_amount` = VALUES(`auto_pixels_amount`), + `auto_gotw_amount` = VALUES(`auto_gotw_amount`), + `auto_points_amount` = VALUES(`auto_points_amount`); + +DROP PROCEDURE IF EXISTS `refresh_permission_definition_rank_columns`; + +DELIMITER $$ +CREATE PROCEDURE `refresh_permission_definition_rank_columns`() +BEGIN + DECLARE done INT DEFAULT 0; + DECLARE current_rank_id INT; + DECLARE current_column_name VARCHAR(32); + DECLARE column_exists INT DEFAULT 0; + DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; + + OPEN rank_cursor; + + rank_loop: LOOP + FETCH rank_cursor INTO current_rank_id; + + IF done = 1 THEN + LEAVE rank_loop; + END IF; + + SET current_column_name = CONCAT('rank_', current_rank_id); + + SELECT COUNT(*) + INTO column_exists + FROM `information_schema`.`columns` + WHERE `table_schema` = DATABASE() + AND `table_name` = 'permission_definitions' + AND `column_name` = current_column_name; + + IF column_exists = 0 THEN + SET @alter_permissions_column_sql = CONCAT( + 'ALTER TABLE `permission_definitions` ADD COLUMN `', + current_column_name, + '` tinyint(3) unsigned NOT NULL DEFAULT 0' + ); + + PREPARE alter_permissions_column_stmt FROM @alter_permissions_column_sql; + EXECUTE alter_permissions_column_stmt; + DEALLOCATE PREPARE alter_permissions_column_stmt; + END IF; + END LOOP; + + CLOSE rank_cursor; +END$$ +DELIMITER ; + +CALL `refresh_permission_definition_rank_columns`(); + +INSERT INTO `permission_definitions` ( + `permission_key`, + `max_value`, + `comment` +) +SELECT + `column_name` AS `permission_key`, + CASE + WHEN `column_type` LIKE '%''2''%' THEN 2 + ELSE 1 + END AS `max_value`, + CASE + WHEN COALESCE(`column_comment`, '') <> '' THEN `column_comment` + WHEN `column_name` LIKE 'cmd\_%' AND `column_type` LIKE '%''2''%' THEN CONCAT( + 'Controls access to the :', + REPLACE(SUBSTRING(`column_name`, 5), '_', ' '), + ' command. Values: 0 = disabled, 1 = allowed, 2 = allowed only when room-owner rights may be used.' + ) + WHEN `column_name` LIKE 'cmd\_%' THEN CONCAT( + 'Controls access to the :', + REPLACE(SUBSTRING(`column_name`, 5), '_', ' '), + ' command. Values: 0 = disabled, 1 = allowed.' + ) + WHEN `column_name` LIKE 'acc\_%' AND `column_type` LIKE '%''2''%' THEN CONCAT( + 'Controls the ', + REPLACE(SUBSTRING(`column_name`, 5), '_', ' '), + ' capability for this rank. Values: 0 = disabled, 1 = enabled, 2 = enabled only when room-owner rights may be used.' + ) + WHEN `column_name` LIKE 'acc\_%' THEN CONCAT( + 'Controls the ', + REPLACE(SUBSTRING(`column_name`, 5), '_', ' '), + ' capability for this rank. Values: 0 = disabled, 1 = enabled.' + ) + ELSE CONCAT( + 'Legacy permission-related value migrated from the old permissions table for ', + `column_name`, + '.' + ) + END AS `comment` +FROM `information_schema`.`columns` +WHERE `table_schema` = DATABASE() + AND `table_name` = 'permissions' + AND `column_name` NOT IN ( + 'id', + 'rank_name', + 'hidden_rank', + 'badge', + 'job_description', + 'staff_color', + 'staff_background', + 'level', + 'room_effect', + 'log_commands', + 'prefix', + 'prefix_color', + 'auto_credits_amount', + 'auto_pixels_amount', + 'auto_gotw_amount', + 'auto_points_amount' + ) +ON DUPLICATE KEY UPDATE + `max_value` = VALUES(`max_value`), + `comment` = VALUES(`comment`); + +DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`; + +CREATE TEMPORARY TABLE `tmp_permission_comments` ( + `permission_key` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL, + `comment` text NOT NULL, + PRIMARY KEY (`permission_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci; + +INSERT INTO `tmp_permission_comments` (`permission_key`, `comment`) VALUES +('cmd_about', 'Allows using :about to display emulator, revision, or hotel information exposed by the command.'), +('cmd_alert', 'Allows using :alert to send a hotel alert popup to a specific user.'), +('cmd_allow_trading', 'Allows using the trading-toggle command to enable or disable trading for a target user.'), +('cmd_badge', 'Allows granting a badge code to a target user through a command.'), +('cmd_ban', 'Allows banning users from the hotel.'), +('cmd_blockalert', 'Allows sending the block-alert style moderation message.'), +('cmd_bots', 'Allows using :bots to list the bots currently placed in the room.'), +('cmd_bundle', 'Allows using :bundle / :roombundle to create a catalog room-bundle offer for the current room.'), +('cmd_calendar', 'Allows using the hotel calendar command and any calendar actions wired to that command entry.'), +('cmd_changename', 'Allows forcing a user-name change through the change-name command flow.'), +('cmd_chatcolor', 'Allows changing the active chat bubble color through the chat-color command.'), +('cmd_commands', 'Allows using :commands to list the command keys available to the current user.'), +('cmd_connect_camera', 'Allows using the command that links the in-room camera feature to the current room session.'), +('cmd_control', 'Allows using :control to take over another in-room user and stop controlling them later.'), +('cmd_coords', 'Allows using :coords to inspect room coordinates for tiles, users, or furniture.'), +('cmd_credits', 'Allows giving or removing credits from a user through the staff currency command.'), +('cmd_subscription', 'Allows granting or editing subscription time through the subscription command.'), +('cmd_danceall', 'Allows forcing every Habbo currently in the room to dance.'), +('cmd_diagonal', 'Allows toggling diagonal walking for the current room.'), +('cmd_disconnect', 'Allows disconnecting a user from the hotel immediately.'), +('cmd_duckets', 'Allows giving or removing duckets from a user through the staff currency command.'), +('cmd_ejectall', 'Allows ejecting all users from the current room.'), +('cmd_empty', 'Allows clearing the current user furniture inventory through the empty-inventory command.'), +('cmd_empty_bots', 'Allows clearing the current user bot inventory through the empty-bots command.'), +('cmd_empty_pets', 'Allows clearing the current user pet inventory through the empty-pets command.'), +('cmd_enable', 'Allows applying an avatar effect to yourself, or to another user when acc_enable_others is also granted.'), +('cmd_event', 'Allows marking the current room as an event room through the event command.'), +('cmd_faceless', 'Allows toggling the faceless avatar visual state on the executing room unit.'), +('cmd_fastwalk', 'Allows toggling fast-walk mode for yourself or another in-room user.'), +('cmd_filterword', 'Allows adding or removing entries from the configured word filter through command usage.'), +('cmd_freeze', 'Allows freezing a target user in place.'), +('cmd_freeze_bots', 'Allows freezing bots that are placed in the room.'), +('cmd_gift', 'Allows sending a gift to a target user through the gift command.'), +('cmd_give_rank', 'Allows setting another user rank through the give-rank command.'), +('cmd_ha', 'Allows sending a hotel-wide alert.'), +('acc_can_stalk', 'Allows following users even when they have disabled stalking.'), +('cmd_hal', 'Allows sending a hotel-wide alert with a clickable link or extended content.'), +('cmd_invisible', 'Allows toggling invisible staff mode.'), +('cmd_ip_ban', 'Allows banning a user by IP address.'), +('cmd_machine_ban', 'Allows banning a user by machine identifier.'), +('cmd_hand_item', 'Allows spawning or changing the hand item currently held by a user.'), +('cmd_happyhour', 'Allows starting or stopping the happy-hour event flow exposed by the happyhour command.'), +('cmd_hidewired', 'Allows toggling whether wired furniture is visually hidden in the current room.'), +('cmd_kickall', 'Allows kicking every user from the current room.'), +('cmd_softkick', 'Allows soft-kicking a user back to the hotel view without a full sanction.'), +('cmd_massbadge', 'Allows giving the same badge to many users at once.'), +('cmd_roombadge', 'Allows setting or overriding the room badge shown to users.'), +('cmd_masscredits', 'Allows giving credits to many users at once through the mass-credits command.'), +('cmd_massduckets', 'Allows giving duckets to many users at once through the mass-duckets command.'), +('cmd_massgift', 'Allows sending the same gift to many users at once.'), +('cmd_masspoints', 'Allows giving activity points to many users at once through the mass-points command.'), +('cmd_moonwalk', 'Allows toggling the moonwalk avatar effect for yourself while you are inside a room.'), +('cmd_mimic', 'Allows copying another user appearance or presence state through the mimic command.'), +('cmd_multi', 'Allows executing multiple chat commands from the special sticky/post-it scripting payload.'), +('cmd_mute', 'Allows muting a target user.'), +('cmd_pet_info', 'Allows opening the detailed pet-information view for a pet.'), +('cmd_pickall', 'Allows picking up every furniture item from the current room.'), +('cmd_plugins', 'Legacy key for the :plugins command, which currently lists loaded plugins without enforcing this dedicated permission node in code.'), +('cmd_points', 'Allows giving or removing activity points from a user through the points command.'), +('cmd_promote_offer', 'Allows using :promoteoffer to list active target offers or switch the globally promoted target offer.'), +('cmd_pull', 'Allows pulling a nearby user onto the tile directly in front of you.'), +('cmd_push', 'Allows pushing the user standing in front of you one tile farther in the direction you are facing.'), +('cmd_redeem', 'Allows redeeming redeemable inventory items through the redeem command flow.'), +('cmd_reload_room', 'Allows unloading and reloading the current room, then forwarding the occupants back into the fresh room instance.'), +('cmd_roomalert', 'Allows sending the same alert message to everyone in the current room.'), +('cmd_roomcredits', 'Allows giving credits to every Habbo currently in the room.'), +('cmd_roomeffect', 'Allows applying the same avatar effect id to every Habbo currently in the room.'), +('cmd_roomgift', 'Allows sending the same gift to every Habbo currently in the room.'), +('cmd_roomitem', 'Allows setting the same hand-item id for every Habbo in the room; using 0 clears the hand item.'), +('cmd_roommute', 'Allows muting every Habbo currently in the room.'), +('cmd_roompixels', 'Allows giving duckets or pixels to every Habbo currently in the room.'), +('cmd_roompoints', 'Allows giving activity points to every Habbo currently in the room.'), +('cmd_say', 'Allows forcing another online user to say a custom message in their current room.'), +('cmd_say_all', 'Allows making everyone in the room say a message.'), +('cmd_setmax', 'Allows using :setmax to change the maximum user capacity of the current room.'), +('cmd_set_poll', 'Allows using :setpoll to attach or remove a poll on the current room.'), +('cmd_setpublic', 'Allows using :setpublic to change the room public/private visibility state.'), +('cmd_setspeed', 'Allows using :setspeed to change the room walking speed setting.'), +('cmd_shout', 'Allows forcing another online user to shout a custom message in their current room.'), +('cmd_shout_all', 'Allows making everyone in the room shout a message.'), +('cmd_shutdown', 'Allows using the shutdown command to stop the emulator process.'), +('cmd_sitdown', 'Allows forcing users to sit down through the sitdown command.'), +('cmd_staffalert', 'Allows sending an alert that is visible only to staff members.'), +('cmd_staffonline', 'Allows viewing the current list of online staff members.'), +('cmd_summon', 'Allows summoning a target user into the room where the staff member currently is.'), +('cmd_summonrank', 'Allows summoning all online users of a given rank into the current room.'), +('cmd_super_ban', 'Allows issuing the strongest ban command variant exposed by the super-ban command.'), +('cmd_stalk', 'Allows following another user to their room.'), +('cmd_superpull', 'Allows pulling a user to the tile in front of you without the short-range reach check used by :pull.'), +('cmd_take_badge', 'Allows removing a badge code from a target user.'), +('cmd_talk', 'Allows using the legacy :talk command to make another user speak a command-provided message.'), +('cmd_teleport', 'Allows toggling the room-unit teleport mode used by the :teleport command.'), +('cmd_trash', 'Allows deleting or trashing furniture/items through the trash command flow.'), +('cmd_transform', 'Allows transforming your room unit into a chosen pet type, race, and color.'), +('cmd_unban', 'Allows removing active bans.'), +('cmd_unload', 'Allows disposing the current room instance immediately through :unload / :crash.'), +('cmd_unmute', 'Allows removing an active mute from a target user.'), +('cmd_update_achievements', 'Allows using :update_achievements to reload achievements configuration.'), +('cmd_update_bots', 'Allows using :update_bots to reload bot data and bot configuration.'), +('cmd_update_catalogue', 'Allows using :update_catalogue to reload catalogue pages and offers.'), +('cmd_update_config', 'Allows using :update_config to reload emulator configuration settings.'), +('cmd_update_guildparts', 'Allows using :update_guildparts to reload guild badge parts and guild configuration.'), +('cmd_update_hotel_view', 'Allows using :update_hotel_view to reload hotel-view assets or settings.'), +('cmd_update_items', 'Allows using :update_items to reload item data and furniture definitions.'), +('cmd_update_navigator', 'Allows using :update_navigator to reload navigator configuration and listings.'), +('cmd_update_permissions', 'Allows using :update_permissions to reload ranks and permissions from the database.'), +('cmd_update_pet_data', 'Allows using :update_pet_data to reload pet types and pet races.'), +('cmd_update_plugins', 'Allows using :update_plugins to reload plugin data or plugin metadata.'), +('cmd_update_polls', 'Allows using :update_polls to reload poll and questionnaire data.'), +('cmd_update_texts', 'Allows using :update_texts to reload external texts and localizations.'), +('cmd_update_wordfilter', 'Allows using :update_wordfilter to reload the word-filter list.'), +('cmd_userinfo', 'Allows opening the detailed user-information view used by staff tools.'), +('cmd_word_quiz', 'Allows starting a room word-quiz event with a custom question and optional duration.'), +('cmd_warp', 'Allows instantly warping your room unit to a target tile.'), +('acc_anychatcolor', 'Allows selecting any chat bubble color, including normally restricted colors.'), +('acc_anyroomowner', 'Treats the rank as room owner for owner-only checks such as room settings, wired saving, rights management, floorplan editing, and similar room-owner gates.'), +('acc_empty_others', 'Allows :empty, :empty_bots, and :empty_pets to target another user inventory instead of only your own.'), +('acc_enable_others', 'Allows :enable to apply avatar effects to another user instead of only to yourself.'), +('acc_see_whispers', 'Allows seeing whispers sent between other users in the room.'), +('acc_see_tentchat', 'Allows seeing tent chat or similar hidden chat channels that are normally not visible to everyone.'), +('acc_superwired', 'Allows saving advanced wired data without the normal wordfilter and reward payload restrictions applied to regular users.'), +('acc_supporttool', 'Allows opening and using the support/moderation tool interface.'), +('acc_unkickable', 'Prevents the user from being kicked by normal moderation or room commands.'), +('acc_guildgate', 'Allows bypassing guild gate access restrictions.'), +('acc_moverotate', 'Allows moving, rotating, and saving wired furniture without the usual room-owner restriction checks.'), +('acc_placefurni', 'Allows placing furniture, opening :wired, and passing room-right checks that normally require owner or controller rights.'), +('acc_unlimited_bots', 'Removes both the bot inventory cap and the per-room bot placement cap for this rank.'), +('acc_unlimited_pets', 'Removes both the pet inventory cap and the per-room pet placement cap for this rank.'), +('acc_hide_ip', 'Hides the user IP address in staff tools and other staff-facing views.'), +('acc_hide_mail', 'Hides the user email address in moderation tools and staff views.'), +('acc_not_mimiced', 'Prevents other users from mimicking this account.'), +('acc_chat_no_flood', 'Exempts the user from flood protection limits.'), +('acc_staff_chat', 'Allows accessing staff-only chat channels and staff broadcasts.'), +('acc_staff_pick', 'Allows using staff item pick-up actions that bypass normal room ownership restrictions.'), +('acc_enteranyroom', 'Allows entering rooms regardless of door mode, bans, or normal access restrictions.'), +('acc_fullrooms', 'Allows entering rooms even when they are at maximum user capacity.'), +('acc_infinite_credits', 'Prevents credits from being consumed when a command or purchase checks credit balance.'), +('acc_infinite_pixels', 'Prevents duckets or pixels from being consumed when the balance is checked.'), +('acc_infinite_points', 'Prevents activity points from being consumed when the balance is checked.'), +('acc_ambassador', 'Marks the rank as an ambassador for ambassador-only tools and visuals.'), +('acc_debug', 'Allows using debug-only features, commands, or internal tooling.'), +('acc_chat_no_limit', 'Lets the user hear and be heard regardless of room hearing distance limits.'), +('acc_chat_no_filter', 'Bypasses the word filter for chat and staff-generated messages.'), +('acc_nomute', 'Prevents the user from being muted by normal mute checks.'), +('acc_guild_admin', 'Allows bypassing guild admin restrictions when managing guilds.'), +('acc_catalog_ids', 'Allows seeing internal catalogue page ids, offer ids, or related technical catalogue identifiers.'), +('acc_modtool_ticket_q', 'Allows seeing and handling the moderation ticket queue.'), +('acc_modtool_user_logs', 'Allows reading user chat logs in the moderation tool.'), +('acc_modtool_user_alert', 'Allows sending moderation alerts or cautions to users.'), +('acc_modtool_user_kick', 'Allows kicking users from the moderation tool.'), +('acc_modtool_user_ban', 'Allows banning users from the moderation tool.'), +('acc_modtool_room_info', 'Allows viewing room information in the moderation tool.'), +('acc_modtool_room_logs', 'Allows viewing room chat logs in the moderation tool.'), +('acc_trade_anywhere', 'Allows starting trades outside the normal trade-enabled areas.'), +('acc_update_notifications', 'Allows receiving update notifications emitted by the emulator.'), +('acc_helper_use_guide_tool', 'Allows opening the helper guide tool.'), +('acc_helper_give_guide_tours', 'Allows accepting and handling guide tour requests.'), +('acc_helper_judge_chat_reviews', 'Allows reviewing helper or chat review tickets.'), +('acc_floorplan_editor', 'Allows opening and saving the floorplan editor.'), +('acc_camera', 'Allows using the in-room camera feature and related camera UI actions.'), +('acc_ads_background', 'Allows editing room advertisement backgrounds.'), +('cmd_wordquiz', 'Legacy alias of cmd_word_quiz for starting a room word-quiz event.'), +('acc_room_staff_tags', 'Shows staff tags or markers above the user while inside rooms.'), +('acc_infinite_friends', 'Removes the normal friend-list size limit.'), +('acc_mimic_unredeemed', 'Allows mimicking looks even when they contain unreleased or restricted clothing.'), +('cmd_update_youtube_playlists', 'Allows reloading YouTube playlist configuration for furniture integrations.'), +('cmd_add_youtube_playlist', 'Allows adding a new YouTube playlist entry.'), +('acc_mention', 'Allows using mention-related chat features beyond the normal rank restriction.'), +('cmd_setstate', 'Legacy room-editor permission for :setstate / :ss, used to change the selected furni state or extradata value.'), +('cmd_buildheight', 'Legacy room-editor permission for :buildheight / :bh, used to change the room build-height override.'), +('cmd_setrotation', 'Legacy room-editor permission for :setrotation / :rot, used to change the rotation of the selected furni.'), +('cmd_sellroom', 'Allows putting the current room up for sale through the sell-room command.'), +('cmd_buyroom', 'Allows purchasing a room that is marked as for sale through the buy-room command.'), +('cmd_pay', 'Allows transferring currency to another user through the pay command.'), +('cmd_kill', 'Allows using the kill command effect exposed by the current command set.'), +('cmd_hoverboard', 'Allows toggling the hoverboard effect or hoverboard movement mode.'), +('cmd_kiss', 'Allows using the kiss interaction command on another user.'), +('cmd_hug', 'Allows using the hug interaction command on another user.'), +('cmd_welcome', 'Allows triggering the welcome command behavior defined by the current command set.'), +('cmd_disable_effects', 'Allows disabling active avatar effects through the disable-effects command.'), +('cmd_brb', 'Allows toggling the be-right-back status command.'), +('cmd_nuke', 'Allows using the nuke command exposed by the current command set.'), +('cmd_slime', 'Allows applying the slime command/effect exposed by the current command set.'), +('cmd_explain', 'Allows using the explain command to send the predefined explanation/help flow to users.'), +('cmd_closedice', 'Legacy essentials permission for :closedice, used to close dice items in the room or all dice at once.'), +('acc_closedice_room', 'Legacy companion permission used by older closed-dice room checks.'), +('cmd_set', 'Legacy essentials permission for :set / :changefurni, the generic furni editing command documented by :set info.'), +('cmd_furnidata', 'Allows viewing technical furnidata information in-game for selected furniture.'), +('kiss_cmd', 'Legacy alias used for the kiss command permission.'), +('acc_calendar_force', 'Allows claiming calendar rewards even when the normal day-difference timing check would block the claim.'), +('cmd_update_calendar', 'Allows using :update_calendar to reload calendar definitions and rewards.'), +('cmd_update_all', 'Allows using :update_all to reload all supported runtime data sets in one command.'), +('cms_dance', 'Legacy CMS-side permission kept for website integrations; no direct in-emulator command handler was found in the current tree.'), +('acc_catalogfurni', 'Allows using catalogue administration features related to furniture pages and offers.'), +('acc_unignorable', 'Prevents the account from being ignored by other users through the ignore system.'), +('cmd_update_chat_bubbles', 'Allows using :update_chat_bubbles to reload chat-bubble definitions and assets.'), +('cmd_calendar_staff', 'Allows the staff-only actions exposed by the calendar command flow.'); + +UPDATE `permission_definitions` pd +INNER JOIN `tmp_permission_comments` tc ON tc.`permission_key` = pd.`permission_key` +SET pd.`comment` = tc.`comment`; + +DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`; + +DROP PROCEDURE IF EXISTS `refresh_permission_definition_values`; + +DELIMITER $$ +CREATE PROCEDURE `refresh_permission_definition_values`() +BEGIN + DECLARE done INT DEFAULT 0; + DECLARE current_rank_id INT; + DECLARE current_column_name VARCHAR(32); + DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; + + OPEN rank_cursor; + + rank_loop: LOOP + FETCH rank_cursor INTO current_rank_id; + + IF done = 1 THEN + LEAVE rank_loop; + END IF; + + SET current_column_name = CONCAT('rank_', current_rank_id); + + SELECT GROUP_CONCAT( + CONCAT( + 'SELECT ''', + REPLACE(`column_name`, '''', ''''''), + ''' AS permission_key, CAST(COALESCE(`', + REPLACE(`column_name`, '`', '``'), + '`, ''0'') AS UNSIGNED) AS permission_value FROM `permissions` WHERE `id` = ', + current_rank_id + ) + ORDER BY `ordinal_position` + SEPARATOR ' UNION ALL ' + ) INTO @permission_rank_source_sql + FROM `information_schema`.`columns` + WHERE `table_schema` = DATABASE() + AND `table_name` = 'permissions' + AND `column_name` NOT IN ( + 'id', + 'rank_name', + 'hidden_rank', + 'badge', + 'job_description', + 'staff_color', + 'staff_background', + 'level', + 'room_effect', + 'log_commands', + 'prefix', + 'prefix_color', + 'auto_credits_amount', + 'auto_pixels_amount', + 'auto_gotw_amount', + 'auto_points_amount' + ); + + SET @permission_rank_update_sql = CONCAT( + 'UPDATE `permission_definitions` pd ', + 'INNER JOIN (', + @permission_rank_source_sql, + ') src ON src.permission_key = pd.permission_key ', + 'SET pd.`', + current_column_name, + '` = src.permission_value' + ); + + PREPARE permission_rank_update_stmt FROM @permission_rank_update_sql; + EXECUTE permission_rank_update_stmt; + DEALLOCATE PREPARE permission_rank_update_stmt; + END LOOP; + + CLOSE rank_cursor; +END$$ +DELIMITER ; + +CALL `refresh_permission_definition_values`(); diff --git a/Database Updates/005_WiredTickService.sql b/Database Updates/005_WiredTickService.sql new file mode 100644 index 00000000..865a40de --- /dev/null +++ b/Database Updates/005_WiredTickService.sql @@ -0,0 +1 @@ +INSERT INTO emulator_settings (`key`, `value`) VALUES ('wired.tick.workers', '6'); \ No newline at end of file diff --git a/Database Updates/005_add_room_wired_settings.sql b/Database Updates/005_add_room_wired_settings.sql new file mode 100644 index 00000000..82c8b8e7 --- /dev/null +++ b/Database Updates/005_add_room_wired_settings.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS `room_wired_settings` ( + `room_id` int(11) NOT NULL, + `inspect_mask` int(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can open and inspect Wired in the room. 1=everyone, 2=users with rights, 4=group members, 8=group admins.', + `modify_mask` int(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can modify Wired in the room. 2=users with rights, 4=group members, 8=group admins.', + PRIMARY KEY (`room_id`), + CONSTRAINT `fk_room_wired_settings_room_id` FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/Database Updates/007_PackageRateLimit.sql b/Database Updates/007_PackageRateLimit.sql new file mode 100644 index 00000000..0e6ffbc8 --- /dev/null +++ b/Database Updates/007_PackageRateLimit.sql @@ -0,0 +1,3 @@ +INSERT INTO emulator_settings (`key`, `value`) VALUES ('packet.global.rate.limit', '50'); + +ALTER TABLE `rooms` ADD COLUMN IF NOT EXISTS `youtube_enabled` TINYINT(1) NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/Database Updates/008_Wired_Update.sql b/Database Updates/008_Wired_Update.sql new file mode 100644 index 00000000..daf373a6 --- /dev/null +++ b/Database Updates/008_Wired_Update.sql @@ -0,0 +1,990 @@ +UPDATE emulator_settings SET `value` = '1' WHERE (`key` = 'wired.engine.enabled'); +UPDATE emulator_settings SET `value` = '1' WHERE (`key` = 'wired.engine.exclusive'); + +ALTER TABLE emulator_settings +ADD COLUMN IF NOT EXISTS `comment` VARCHAR(255) NOT NULL AFTER `value`; + + +CREATE TABLE IF NOT EXISTS `catalog_items_bc` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `item_ids` varchar(666) NOT NULL, + `page_id` int(11) NOT NULL, + `catalog_name` varchar(100) NOT NULL, + `order_number` int(11) NOT NULL DEFAULT 1, + `extradata` varchar(500) NOT NULL DEFAULT '', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE IF NOT EXISTS `catalog_pages_bc` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `parent_id` int(11) NOT NULL DEFAULT -1, + `caption` varchar(128) NOT NULL, + `page_layout` enum( + 'default_3x3','club_buy','club_gift','frontpage','spaces','recycler', + 'recycler_info','recycler_prizes','trophies','plasto','marketplace', + 'marketplace_own_items','spaces_new','soundmachine','guilds','guild_furni', + 'info_duckets','info_rentables','info_pets','roomads','single_bundle', + 'sold_ltd_items','badge_display','bots','pets','pets2','pets3', + 'productpage1','room_bundle','recent_purchases', + 'default_3x3_color_grouping','guild_forum','vip_buy','info_loyalty', + 'loyalty_vip_buy','collectibles','petcustomization','frontpage_featured' + ) NOT NULL DEFAULT 'default_3x3', + `icon_color` int(11) NOT NULL DEFAULT 1, + `icon_image` int(11) NOT NULL DEFAULT 1, + `order_num` int(11) NOT NULL DEFAULT 1, + `visible` enum('0','1') NOT NULL DEFAULT '1', + `enabled` enum('0','1') NOT NULL DEFAULT '1', + `page_headline` varchar(1024) NOT NULL DEFAULT '', + `page_teaser` varchar(64) NOT NULL DEFAULT '', + `page_special` varchar(2048) DEFAULT '' COMMENT 'Gold Bubble: catalog_special_txtbg1 // Speech Bubble: catalog_special_txtbg2 // Place normal text in page_text_teaser', + `page_text1` text DEFAULT NULL, + `page_text2` text DEFAULT NULL, + `page_text_details` text DEFAULT NULL, + `page_text_teaser` text DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci ROW_FORMAT=DYNAMIC; + +ALTER TABLE `catalog_club_offers` +MODIFY COLUMN `type` ENUM('HC','VIP','BUILDERS_CLUB','BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC'; + +ALTER TABLE `catalog_pages` + MODIFY COLUMN `page_layout` ENUM( + 'default_3x3', + 'club_buy', + 'club_gift', + 'frontpage', + 'spaces', + 'recycler', + 'recycler_info', + 'recycler_prizes', + 'trophies', + 'plasto', + 'marketplace', + 'marketplace_own_items', + 'spaces_new', + 'soundmachine', + 'guilds', + 'guild_furni', + 'info_duckets', + 'info_rentables', + 'info_pets', + 'roomads', + 'single_bundle', + 'sold_ltd_items', + 'badge_display', + 'bots', + 'pets', + 'pets2', + 'pets3', + 'productpage1', + 'room_bundle', + 'recent_purchases', + 'default_3x3_color_grouping', + 'guild_forum', + 'vip_buy', + 'info_loyalty', + 'loyalty_vip_buy', + 'collectibles', + 'petcustomization', + 'frontpage_featured', + 'builders_club_frontpage', + 'builders_club_addons', + 'builders_club_loyalty' + ) NOT NULL DEFAULT 'default_3x3'; + +ALTER TABLE `catalog_pages` +ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL','BUILDER','BOTH') NOT NULL DEFAULT 'NORMAL' +AFTER `club_only`; + +ALTER TABLE `catalog_pages_bc` + MODIFY COLUMN `page_layout` ENUM( + 'default_3x3', + 'club_buy', + 'club_gift', + 'frontpage', + 'spaces', + 'recycler', + 'recycler_info', + 'recycler_prizes', + 'trophies', + 'plasto', + 'marketplace', + 'marketplace_own_items', + 'spaces_new', + 'soundmachine', + 'guilds', + 'guild_furni', + 'info_duckets', + 'info_rentables', + 'info_pets', + 'roomads', + 'single_bundle', + 'sold_ltd_items', + 'badge_display', + 'bots', + 'pets', + 'pets2', + 'pets3', + 'productpage1', + 'room_bundle', + 'recent_purchases', + 'default_3x3_color_grouping', + 'guild_forum', + 'vip_buy', + 'info_loyalty', + 'loyalty_vip_buy', + 'collectibles', + 'petcustomization', + 'frontpage_featured', + 'builders_club_frontpage', + 'builders_club_addons', + 'builders_club_loyalty' + ) NOT NULL DEFAULT 'default_3x3'; + +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'users_settings' + AND COLUMN_NAME = 'builders_club_bonus_furni' +); +SET @sql := IF(@col_exists = 0, + 'ALTER TABLE `users_settings` ADD COLUMN `builders_club_bonus_furni` INT NOT NULL DEFAULT 0;', + 'SELECT "exists";' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +CREATE TABLE IF NOT EXISTS `wired_emulator_settings` ( + `key` varchar(191) NOT NULL, + `value` text NOT NULL, + `comment` text NOT NULL, + PRIMARY KEY (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; + +INSERT INTO `wired_emulator_settings` (`key`, `value`, `comment`) +SELECT 'wired.engine.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.enabled' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.' +UNION ALL +SELECT 'wired.engine.exclusive', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.exclusive' LIMIT 1), '1'), 'Compatibility flag kept for older configs. The runtime now always uses the new wired engine.' +UNION ALL +SELECT 'wired.engine.maxStepsPerStack', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.maxStepsPerStack' LIMIT 1), '100'), 'Maximum amount of internal processing steps allowed for a single wired stack execution.' +UNION ALL +SELECT 'wired.engine.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.engine.debug' LIMIT 1), '0'), 'Enable verbose debug logging for the new wired engine.' +UNION ALL +SELECT 'wired.custom.enabled', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.custom.enabled' LIMIT 1), '0'), 'Enable custom legacy wired behaviour such as user-based cooldown exceptions and compatibility logic.' +UNION ALL +SELECT 'hotel.wired.furni.selection.count', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.furni.selection.count' LIMIT 1), '5'), 'Maximum number of furni that a wired box can store or select.' +UNION ALL +SELECT 'hotel.wired.max_delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.max_delay' LIMIT 1), '20'), 'Maximum delay value accepted by wired effects that support delayed execution.' +UNION ALL +SELECT 'hotel.wired.message.max_length', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'hotel.wired.message.max_length' LIMIT 1), '100'), 'Maximum length of text fields used by wired messages and bot text effects.' +UNION ALL +SELECT 'wired.effect.teleport.delay', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.effect.teleport.delay' LIMIT 1), '500'), 'Delay in milliseconds used by wired teleport movement.' +UNION ALL +SELECT 'wired.place.under', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.place.under' LIMIT 1), '0'), 'Allow placing wired furniture underneath other items when room rules permit it.' +UNION ALL +SELECT 'wired.tick.interval.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.interval.ms' LIMIT 1), '50'), 'Global wired tick interval in milliseconds used by repeaters and other tick-driven wired items.' +UNION ALL +SELECT 'wired.tick.resolution', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.resolution' LIMIT 1), '100'), 'Legacy wired tick resolution value kept for compatibility with older wired timing setups.' +UNION ALL +SELECT 'wired.tick.debug', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.debug' LIMIT 1), '0'), 'Enable verbose logging for the wired tick service.' +UNION ALL +SELECT 'wired.tick.thread.priority', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.tick.thread.priority' LIMIT 1), '6'), 'Java thread priority used by the wired tick service.' +UNION ALL +SELECT 'wired.highscores.displaycount', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.highscores.displaycount' LIMIT 1), '25'), 'Maximum number of wired highscore entries shown to users when a highscore is displayed.' +UNION ALL +SELECT 'wired.abuse.max.recursion.depth', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.recursion.depth' LIMIT 1), '10'), 'Maximum recursive wired depth allowed before execution is stopped.' +UNION ALL +SELECT 'wired.abuse.max.events.per.window', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.max.events.per.window' LIMIT 1), '100'), 'Maximum amount of identical wired events allowed inside the abuse rate-limit window before a room ban is applied.' +UNION ALL +SELECT 'wired.abuse.rate.limit.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.rate.limit.window.ms' LIMIT 1), '10000'), 'Time window in milliseconds used by the wired abuse rate limiter.' +UNION ALL +SELECT 'wired.abuse.ban.duration.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.abuse.ban.duration.ms' LIMIT 1), '600000'), 'Duration in milliseconds of the temporary wired ban after abuse detection.' +UNION ALL +SELECT 'wired.monitor.usage.window.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.window.ms' LIMIT 1), '1000'), 'Rolling window size in milliseconds used to calculate wired usage in the :wired monitor.' +UNION ALL +SELECT 'wired.monitor.usage.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.usage.limit' LIMIT 1), '1000'), 'Maximum wired usage budget allowed in one monitor window before EXECUTION_CAP is raised.' +UNION ALL +SELECT 'wired.monitor.delayed.events.limit', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.delayed.events.limit' LIMIT 1), '100'), 'Maximum number of delayed wired events that can be queued in one room at the same time.' +UNION ALL +SELECT 'wired.monitor.overload.average.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.average.ms' LIMIT 1), '50'), 'Average execution time threshold in milliseconds that starts overload tracking.' +UNION ALL +SELECT 'wired.monitor.overload.peak.ms', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.peak.ms' LIMIT 1), '150'), 'Peak single execution time threshold in milliseconds that starts overload tracking.' +UNION ALL +SELECT 'wired.monitor.overload.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.overload.consecutive.windows' LIMIT 1), '2'), 'Number of consecutive overloaded monitor windows required before logging EXECUTOR_OVERLOAD.' +UNION ALL +SELECT 'wired.monitor.heavy.usage.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.usage.percent' LIMIT 1), '70'), 'Usage percentage threshold that contributes to marking a room as heavy in the :wired monitor.' +UNION ALL +SELECT 'wired.monitor.heavy.consecutive.windows', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.consecutive.windows' LIMIT 1), '5'), 'Number of consecutive windows above the heavy usage threshold required before the room is marked as heavy.' +UNION ALL +SELECT 'wired.monitor.heavy.delayed.percent', COALESCE((SELECT `value` FROM `emulator_settings` WHERE `key` = 'wired.monitor.heavy.delayed.percent' LIMIT 1), '60'), 'Delayed queue percentage threshold that also contributes to the heavy-room calculation.' +ON DUPLICATE KEY UPDATE + `value` = VALUES(`value`), + `comment` = VALUES(`comment`); + +DELETE FROM `emulator_settings` +WHERE `key` IN ( + 'wired.engine.enabled', + 'wired.engine.exclusive', + 'wired.engine.maxStepsPerStack', + 'wired.engine.debug', + 'wired.custom.enabled', + 'hotel.wired.furni.selection.count', + 'hotel.wired.max_delay', + 'hotel.wired.message.max_length', + 'wired.effect.teleport.delay', + 'wired.place.under', + 'wired.tick.interval.ms', + 'wired.tick.resolution', + 'wired.tick.debug', + 'wired.tick.thread.priority', + 'wired.highscores.displaycount', + 'wired.abuse.max.recursion.depth', + 'wired.abuse.max.events.per.window', + 'wired.abuse.rate.limit.window.ms', + 'wired.abuse.ban.duration.ms', + 'wired.monitor.usage.window.ms', + 'wired.monitor.usage.limit', + 'wired.monitor.delayed.events.limit', + 'wired.monitor.overload.average.ms', + 'wired.monitor.overload.peak.ms', + 'wired.monitor.overload.consecutive.windows', + 'wired.monitor.heavy.usage.percent', + 'wired.monitor.heavy.consecutive.windows', + 'wired.monitor.heavy.delayed.percent' +); + +UPDATE `emulator_settings` SET `comment` = 'Allow whispering while a user stands inside a mute area.' WHERE `key` = 'room.chat.mutearea.allow_whisper'; +UPDATE `emulator_settings` SET `comment` = 'HTML or text format used for room chat prefixes.' WHERE `key` = 'room.chat.prefix.format'; +UPDATE `emulator_settings` SET `comment` = 'Badge code displayed on promoted rooms.' WHERE `key` = 'room.promotion.badge'; +UPDATE `emulator_settings` SET `comment` = 'Image used by Rosie bubble notifications.' WHERE `key` = 'rosie.bubble.image.url'; +UPDATE `emulator_settings` SET `comment` = 'Currency type used by Rosie when buying a room or room package.' WHERE `key` = 'rosie.buyroom.currency.type'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `runtime.threads`.' WHERE `key` = 'runtime.threads'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.private.chats`.' WHERE `key` = 'save.private.chats'; +UPDATE `emulator_settings` SET `comment` = 'Configuration value used by `save.room.chats`.' WHERE `key` = 'save.room.chats'; +UPDATE `emulator_settings` SET `comment` = 'Expose moderation tickets to the scripter or automation tooling.' WHERE `key` = 'scripter.modtool.tickets'; +UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for diamonds.' WHERE `key` = 'seasonal.currency.diamond'; +UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for duckets.' WHERE `key` = 'seasonal.currency.ducket'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated display names for seasonal currency types.' WHERE `key` = 'seasonal.currency.names'; +UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for pixels.' WHERE `key` = 'seasonal.currency.pixel'; +UPDATE `emulator_settings` SET `comment` = 'Currency type ID used for shells.' WHERE `key` = 'seasonal.currency.shell'; +UPDATE `emulator_settings` SET `comment` = 'Primary seasonal currency type ID.' WHERE `key` = 'seasonal.primary.type'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated list of currency type IDs treated as seasonal currencies.' WHERE `key` = 'seasonal.types'; +UPDATE `emulator_settings` SET `comment` = 'Achievement code granted for the HC subscription tier.' WHERE `key` = 'subscriptions.hc.achievement'; +UPDATE `emulator_settings` SET `comment` = 'Number of days before expiry when HC discount offers become available.' WHERE `key` = 'subscriptions.hc.discount.days_before_end'; +UPDATE `emulator_settings` SET `comment` = 'Enable discounted HC renewal offers.' WHERE `key` = 'subscriptions.hc.discount.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Reset tracked credits spent when the HC subscription expires.' WHERE `key` = 'subscriptions.hc.payday.creditsspent_reset_on_expire'; +UPDATE `emulator_settings` SET `comment` = 'Currency rewarded by the HC payday system.' WHERE `key` = 'subscriptions.hc.payday.currency'; +UPDATE `emulator_settings` SET `comment` = 'Enable the HC payday reward system.' WHERE `key` = 'subscriptions.hc.payday.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Date interval used between HC payday reward runs.' WHERE `key` = 'subscriptions.hc.payday.interval'; +UPDATE `emulator_settings` SET `comment` = 'Next scheduled execution date for HC payday rewards.' WHERE `key` = 'subscriptions.hc.payday.next_date'; +UPDATE `emulator_settings` SET `comment` = 'Percentage of eligible spending returned by HC payday.' WHERE `key` = 'subscriptions.hc.payday.percentage'; +UPDATE `emulator_settings` SET `comment` = 'Semicolon-separated streak thresholds and rewards for HC payday.' WHERE `key` = 'subscriptions.hc.payday.streak'; +UPDATE `emulator_settings` SET `comment` = 'Enable the subscription background scheduler.' WHERE `key` = 'subscriptions.scheduler.enabled'; +UPDATE `emulator_settings` SET `comment` = 'Interval in minutes between subscription scheduler runs.' WHERE `key` = 'subscriptions.scheduler.interval'; +UPDATE `emulator_settings` SET `comment` = 'Compatibility marker used by the custom team wired implementation. Do not remove.' WHERE `key` = 'team.wired.update.rc-1'; +UPDATE `emulator_settings` SET `comment` = 'API key used by the YouTube integration.' WHERE `key` = 'youtube.apikey'; + +DROP VIEW IF EXISTS `permissions_matrix_view`; +DROP PROCEDURE IF EXISTS `refresh_permissions_matrix_view`; +DROP TABLE IF EXISTS `permission_rank_values`; +DROP TABLE IF EXISTS `permission_nodes`; + +CREATE TABLE IF NOT EXISTS `permission_ranks` ( + `id` int(11) NOT NULL, + `rank_name` varchar(25) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL, + `hidden_rank` tinyint(1) NOT NULL DEFAULT 0, + `badge` varchar(12) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '', + `job_description` varchar(255) NOT NULL DEFAULT 'Here to help', + `staff_color` varchar(8) NOT NULL DEFAULT '#327fa8', + `staff_background` varchar(255) NOT NULL DEFAULT 'staff-bg.png', + `level` int(11) NOT NULL DEFAULT 1, + `room_effect` int(11) NOT NULL DEFAULT 0, + `log_commands` enum('0','1') CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '0', + `prefix` varchar(5) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '', + `prefix_color` varchar(7) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '', + `auto_credits_amount` int(11) DEFAULT 0, + `auto_pixels_amount` int(11) DEFAULT 0, + `auto_gotw_amount` int(11) DEFAULT 0, + `auto_points_amount` int(11) DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE IF NOT EXISTS `permission_definitions` ( + `permission_key` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL, + `max_value` tinyint(3) unsigned NOT NULL DEFAULT 1, + `comment` text NOT NULL, + PRIMARY KEY (`permission_key`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci ROW_FORMAT=DYNAMIC; + +ALTER TABLE `permission_definitions` + DROP COLUMN IF EXISTS `category`, + DROP COLUMN IF EXISTS `value_type`, + DROP COLUMN IF EXISTS `sort_order`; + +INSERT INTO `permission_ranks` ( + `id`, + `rank_name`, + `hidden_rank`, + `badge`, + `job_description`, + `staff_color`, + `staff_background`, + `level`, + `room_effect`, + `log_commands`, + `prefix`, + `prefix_color`, + `auto_credits_amount`, + `auto_pixels_amount`, + `auto_gotw_amount`, + `auto_points_amount` +) +SELECT + `id`, + `rank_name`, + `hidden_rank`, + `badge`, + `job_description`, + `staff_color`, + `staff_background`, + `level`, + `room_effect`, + `log_commands`, + `prefix`, + `prefix_color`, + `auto_credits_amount`, + `auto_pixels_amount`, + `auto_gotw_amount`, + `auto_points_amount` +FROM `permissions` +ON DUPLICATE KEY UPDATE + `rank_name` = VALUES(`rank_name`), + `hidden_rank` = VALUES(`hidden_rank`), + `badge` = VALUES(`badge`), + `job_description` = VALUES(`job_description`), + `staff_color` = VALUES(`staff_color`), + `staff_background` = VALUES(`staff_background`), + `level` = VALUES(`level`), + `room_effect` = VALUES(`room_effect`), + `log_commands` = VALUES(`log_commands`), + `prefix` = VALUES(`prefix`), + `prefix_color` = VALUES(`prefix_color`), + `auto_credits_amount` = VALUES(`auto_credits_amount`), + `auto_pixels_amount` = VALUES(`auto_pixels_amount`), + `auto_gotw_amount` = VALUES(`auto_gotw_amount`), + `auto_points_amount` = VALUES(`auto_points_amount`); + +DROP PROCEDURE IF EXISTS `refresh_permission_definition_rank_columns`; + +DELIMITER $$ +CREATE PROCEDURE `refresh_permission_definition_rank_columns`() +BEGIN + DECLARE done INT DEFAULT 0; + DECLARE current_rank_id INT; + DECLARE current_column_name VARCHAR(32); + DECLARE column_exists INT DEFAULT 0; + DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; + + OPEN rank_cursor; + + rank_loop: LOOP + FETCH rank_cursor INTO current_rank_id; + + IF done = 1 THEN + LEAVE rank_loop; + END IF; + + SET current_column_name = CONCAT('rank_', current_rank_id); + + SELECT COUNT(*) + INTO column_exists + FROM `information_schema`.`columns` + WHERE `table_schema` = DATABASE() + AND `table_name` = 'permission_definitions' + AND `column_name` = current_column_name; + + IF column_exists = 0 THEN + SET @alter_permissions_column_sql = CONCAT( + 'ALTER TABLE `permission_definitions` ADD COLUMN `', + current_column_name, + '` tinyint(3) unsigned NOT NULL DEFAULT 0' + ); + + PREPARE alter_permissions_column_stmt FROM @alter_permissions_column_sql; + EXECUTE alter_permissions_column_stmt; + DEALLOCATE PREPARE alter_permissions_column_stmt; + END IF; + END LOOP; + + CLOSE rank_cursor; +END$$ +DELIMITER ; + +CALL `refresh_permission_definition_rank_columns`(); + +INSERT INTO `permission_definitions` ( + `permission_key`, + `max_value`, + `comment` +) +SELECT + `column_name` AS `permission_key`, + CASE + WHEN `column_type` LIKE '%''2''%' THEN 2 + ELSE 1 + END AS `max_value`, + CASE + WHEN COALESCE(`column_comment`, '') <> '' THEN `column_comment` + WHEN `column_name` LIKE 'cmd\_%' AND `column_type` LIKE '%''2''%' THEN CONCAT( + 'Controls access to the :', + REPLACE(SUBSTRING(`column_name`, 5), '_', ' '), + ' command. Values: 0 = disabled, 1 = allowed, 2 = allowed only when room-owner rights may be used.' + ) + WHEN `column_name` LIKE 'cmd\_%' THEN CONCAT( + 'Controls access to the :', + REPLACE(SUBSTRING(`column_name`, 5), '_', ' '), + ' command. Values: 0 = disabled, 1 = allowed.' + ) + WHEN `column_name` LIKE 'acc\_%' AND `column_type` LIKE '%''2''%' THEN CONCAT( + 'Controls the ', + REPLACE(SUBSTRING(`column_name`, 5), '_', ' '), + ' capability for this rank. Values: 0 = disabled, 1 = enabled, 2 = enabled only when room-owner rights may be used.' + ) + WHEN `column_name` LIKE 'acc\_%' THEN CONCAT( + 'Controls the ', + REPLACE(SUBSTRING(`column_name`, 5), '_', ' '), + ' capability for this rank. Values: 0 = disabled, 1 = enabled.' + ) + ELSE CONCAT( + 'Legacy permission-related value migrated from the old permissions table for ', + `column_name`, + '.' + ) + END AS `comment` +FROM `information_schema`.`columns` +WHERE `table_schema` = DATABASE() + AND `table_name` = 'permissions' + AND `column_name` NOT IN ( + 'id', + 'rank_name', + 'hidden_rank', + 'badge', + 'job_description', + 'staff_color', + 'staff_background', + 'level', + 'room_effect', + 'log_commands', + 'prefix', + 'prefix_color', + 'auto_credits_amount', + 'auto_pixels_amount', + 'auto_gotw_amount', + 'auto_points_amount' + ) +ON DUPLICATE KEY UPDATE + `max_value` = VALUES(`max_value`), + `comment` = VALUES(`comment`); + +DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`; + +CREATE TEMPORARY TABLE `tmp_permission_comments` ( + `permission_key` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL, + `comment` text NOT NULL, + PRIMARY KEY (`permission_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_uca1400_ai_ci; + +INSERT INTO `tmp_permission_comments` (`permission_key`, `comment`) VALUES +('cmd_about', 'Allows using :about to display emulator, revision, or hotel information exposed by the command.'), +('cmd_alert', 'Allows using :alert to send a hotel alert popup to a specific user.'), +('cmd_allow_trading', 'Allows using the trading-toggle command to enable or disable trading for a target user.'), +('cmd_badge', 'Allows granting a badge code to a target user through a command.'), +('cmd_ban', 'Allows banning users from the hotel.'), +('cmd_blockalert', 'Allows sending the block-alert style moderation message.'), +('cmd_bots', 'Allows using :bots to list the bots currently placed in the room.'), +('cmd_bundle', 'Allows using :bundle / :roombundle to create a catalog room-bundle offer for the current room.'), +('cmd_calendar', 'Allows using the hotel calendar command and any calendar actions wired to that command entry.'), +('cmd_changename', 'Allows forcing a user-name change through the change-name command flow.'), +('cmd_chatcolor', 'Allows changing the active chat bubble color through the chat-color command.'), +('cmd_commands', 'Allows using :commands to list the command keys available to the current user.'), +('cmd_connect_camera', 'Allows using the command that links the in-room camera feature to the current room session.'), +('cmd_control', 'Allows using :control to take over another in-room user and stop controlling them later.'), +('cmd_coords', 'Allows using :coords to inspect room coordinates for tiles, users, or furniture.'), +('cmd_credits', 'Allows giving or removing credits from a user through the staff currency command.'), +('cmd_subscription', 'Allows granting or editing subscription time through the subscription command.'), +('cmd_danceall', 'Allows forcing every Habbo currently in the room to dance.'), +('cmd_diagonal', 'Allows toggling diagonal walking for the current room.'), +('cmd_disconnect', 'Allows disconnecting a user from the hotel immediately.'), +('cmd_duckets', 'Allows giving or removing duckets from a user through the staff currency command.'), +('cmd_ejectall', 'Allows ejecting all users from the current room.'), +('cmd_empty', 'Allows clearing the current user furniture inventory through the empty-inventory command.'), +('cmd_empty_bots', 'Allows clearing the current user bot inventory through the empty-bots command.'), +('cmd_empty_pets', 'Allows clearing the current user pet inventory through the empty-pets command.'), +('cmd_enable', 'Allows applying an avatar effect to yourself, or to another user when acc_enable_others is also granted.'), +('cmd_event', 'Allows marking the current room as an event room through the event command.'), +('cmd_faceless', 'Allows toggling the faceless avatar visual state on the executing room unit.'), +('cmd_fastwalk', 'Allows toggling fast-walk mode for yourself or another in-room user.'), +('cmd_filterword', 'Allows adding or removing entries from the configured word filter through command usage.'), +('cmd_freeze', 'Allows freezing a target user in place.'), +('cmd_freeze_bots', 'Allows freezing bots that are placed in the room.'), +('cmd_gift', 'Allows sending a gift to a target user through the gift command.'), +('cmd_give_rank', 'Allows setting another user rank through the give-rank command.'), +('cmd_ha', 'Allows sending a hotel-wide alert.'), +('acc_can_stalk', 'Allows following users even when they have disabled stalking.'), +('cmd_hal', 'Allows sending a hotel-wide alert with a clickable link or extended content.'), +('cmd_invisible', 'Allows toggling invisible staff mode.'), +('cmd_ip_ban', 'Allows banning a user by IP address.'), +('cmd_machine_ban', 'Allows banning a user by machine identifier.'), +('cmd_hand_item', 'Allows spawning or changing the hand item currently held by a user.'), +('cmd_happyhour', 'Allows starting or stopping the happy-hour event flow exposed by the happyhour command.'), +('cmd_hidewired', 'Allows toggling whether wired furniture is visually hidden in the current room.'), +('cmd_kickall', 'Allows kicking every user from the current room.'), +('cmd_softkick', 'Allows soft-kicking a user back to the hotel view without a full sanction.'), +('cmd_massbadge', 'Allows giving the same badge to many users at once.'), +('cmd_roombadge', 'Allows setting or overriding the room badge shown to users.'), +('cmd_masscredits', 'Allows giving credits to many users at once through the mass-credits command.'), +('cmd_massduckets', 'Allows giving duckets to many users at once through the mass-duckets command.'), +('cmd_massgift', 'Allows sending the same gift to many users at once.'), +('cmd_masspoints', 'Allows giving activity points to many users at once through the mass-points command.'), +('cmd_moonwalk', 'Allows toggling the moonwalk avatar effect for yourself while you are inside a room.'), +('cmd_mimic', 'Allows copying another user appearance or presence state through the mimic command.'), +('cmd_multi', 'Allows executing multiple chat commands from the special sticky/post-it scripting payload.'), +('cmd_mute', 'Allows muting a target user.'), +('cmd_pet_info', 'Allows opening the detailed pet-information view for a pet.'), +('cmd_pickall', 'Allows picking up every furniture item from the current room.'), +('cmd_plugins', 'Legacy key for the :plugins command, which currently lists loaded plugins without enforcing this dedicated permission node in code.'), +('cmd_points', 'Allows giving or removing activity points from a user through the points command.'), +('cmd_promote_offer', 'Allows using :promoteoffer to list active target offers or switch the globally promoted target offer.'), +('cmd_pull', 'Allows pulling a nearby user onto the tile directly in front of you.'), +('cmd_push', 'Allows pushing the user standing in front of you one tile farther in the direction you are facing.'), +('cmd_redeem', 'Allows redeeming redeemable inventory items through the redeem command flow.'), +('cmd_reload_room', 'Allows unloading and reloading the current room, then forwarding the occupants back into the fresh room instance.'), +('cmd_roomalert', 'Allows sending the same alert message to everyone in the current room.'), +('cmd_roomcredits', 'Allows giving credits to every Habbo currently in the room.'), +('cmd_roomeffect', 'Allows applying the same avatar effect id to every Habbo currently in the room.'), +('cmd_roomgift', 'Allows sending the same gift to every Habbo currently in the room.'), +('cmd_roomitem', 'Allows setting the same hand-item id for every Habbo in the room; using 0 clears the hand item.'), +('cmd_roommute', 'Allows muting every Habbo currently in the room.'), +('cmd_roompixels', 'Allows giving duckets or pixels to every Habbo currently in the room.'), +('cmd_roompoints', 'Allows giving activity points to every Habbo currently in the room.'), +('cmd_say', 'Allows forcing another online user to say a custom message in their current room.'), +('cmd_say_all', 'Allows making everyone in the room say a message.'), +('cmd_setmax', 'Allows using :setmax to change the maximum user capacity of the current room.'), +('cmd_set_poll', 'Allows using :setpoll to attach or remove a poll on the current room.'), +('cmd_setpublic', 'Allows using :setpublic to change the room public/private visibility state.'), +('cmd_setspeed', 'Allows using :setspeed to change the room walking speed setting.'), +('cmd_shout', 'Allows forcing another online user to shout a custom message in their current room.'), +('cmd_shout_all', 'Allows making everyone in the room shout a message.'), +('cmd_shutdown', 'Allows using the shutdown command to stop the emulator process.'), +('cmd_sitdown', 'Allows forcing users to sit down through the sitdown command.'), +('cmd_staffalert', 'Allows sending an alert that is visible only to staff members.'), +('cmd_staffonline', 'Allows viewing the current list of online staff members.'), +('cmd_summon', 'Allows summoning a target user into the room where the staff member currently is.'), +('cmd_summonrank', 'Allows summoning all online users of a given rank into the current room.'), +('cmd_super_ban', 'Allows issuing the strongest ban command variant exposed by the super-ban command.'), +('cmd_stalk', 'Allows following another user to their room.'), +('cmd_superpull', 'Allows pulling a user to the tile in front of you without the short-range reach check used by :pull.'), +('cmd_take_badge', 'Allows removing a badge code from a target user.'), +('cmd_talk', 'Allows using the legacy :talk command to make another user speak a command-provided message.'), +('cmd_teleport', 'Allows toggling the room-unit teleport mode used by the :teleport command.'), +('cmd_trash', 'Allows deleting or trashing furniture/items through the trash command flow.'), +('cmd_transform', 'Allows transforming your room unit into a chosen pet type, race, and color.'), +('cmd_unban', 'Allows removing active bans.'), +('cmd_unload', 'Allows disposing the current room instance immediately through :unload / :crash.'), +('cmd_unmute', 'Allows removing an active mute from a target user.'), +('cmd_update_achievements', 'Allows using :update_achievements to reload achievements configuration.'), +('cmd_update_bots', 'Allows using :update_bots to reload bot data and bot configuration.'), +('cmd_update_catalogue', 'Allows using :update_catalogue to reload catalogue pages and offers.'), +('cmd_update_config', 'Allows using :update_config to reload emulator configuration settings.'), +('cmd_update_guildparts', 'Allows using :update_guildparts to reload guild badge parts and guild configuration.'), +('cmd_update_hotel_view', 'Allows using :update_hotel_view to reload hotel-view assets or settings.'), +('cmd_update_items', 'Allows using :update_items to reload item data and furniture definitions.'), +('cmd_update_navigator', 'Allows using :update_navigator to reload navigator configuration and listings.'), +('cmd_update_permissions', 'Allows using :update_permissions to reload ranks and permissions from the database.'), +('cmd_update_pet_data', 'Allows using :update_pet_data to reload pet types and pet races.'), +('cmd_update_plugins', 'Allows using :update_plugins to reload plugin data or plugin metadata.'), +('cmd_update_polls', 'Allows using :update_polls to reload poll and questionnaire data.'), +('cmd_update_texts', 'Allows using :update_texts to reload external texts and localizations.'), +('cmd_update_wordfilter', 'Allows using :update_wordfilter to reload the word-filter list.'), +('cmd_userinfo', 'Allows opening the detailed user-information view used by staff tools.'), +('cmd_word_quiz', 'Allows starting a room word-quiz event with a custom question and optional duration.'), +('cmd_warp', 'Allows instantly warping your room unit to a target tile.'), +('acc_anychatcolor', 'Allows selecting any chat bubble color, including normally restricted colors.'), +('acc_anyroomowner', 'Treats the rank as room owner for owner-only checks such as room settings, wired saving, rights management, floorplan editing, and similar room-owner gates.'), +('acc_empty_others', 'Allows :empty, :empty_bots, and :empty_pets to target another user inventory instead of only your own.'), +('acc_enable_others', 'Allows :enable to apply avatar effects to another user instead of only to yourself.'), +('acc_see_whispers', 'Allows seeing whispers sent between other users in the room.'), +('acc_see_tentchat', 'Allows seeing tent chat or similar hidden chat channels that are normally not visible to everyone.'), +('acc_superwired', 'Allows saving advanced wired data without the normal wordfilter and reward payload restrictions applied to regular users.'), +('acc_supporttool', 'Allows opening and using the support/moderation tool interface.'), +('acc_unkickable', 'Prevents the user from being kicked by normal moderation or room commands.'), +('acc_guildgate', 'Allows bypassing guild gate access restrictions.'), +('acc_moverotate', 'Allows moving, rotating, and saving wired furniture without the usual room-owner restriction checks.'), +('acc_placefurni', 'Allows placing furniture, opening :wired, and passing room-right checks that normally require owner or controller rights.'), +('acc_unlimited_bots', 'Removes both the bot inventory cap and the per-room bot placement cap for this rank.'), +('acc_unlimited_pets', 'Removes both the pet inventory cap and the per-room pet placement cap for this rank.'), +('acc_hide_ip', 'Hides the user IP address in staff tools and other staff-facing views.'), +('acc_hide_mail', 'Hides the user email address in moderation tools and staff views.'), +('acc_not_mimiced', 'Prevents other users from mimicking this account.'), +('acc_chat_no_flood', 'Exempts the user from flood protection limits.'), +('acc_staff_chat', 'Allows accessing staff-only chat channels and staff broadcasts.'), +('acc_staff_pick', 'Allows using staff item pick-up actions that bypass normal room ownership restrictions.'), +('acc_enteranyroom', 'Allows entering rooms regardless of door mode, bans, or normal access restrictions.'), +('acc_fullrooms', 'Allows entering rooms even when they are at maximum user capacity.'), +('acc_infinite_credits', 'Prevents credits from being consumed when a command or purchase checks credit balance.'), +('acc_infinite_pixels', 'Prevents duckets or pixels from being consumed when the balance is checked.'), +('acc_infinite_points', 'Prevents activity points from being consumed when the balance is checked.'), +('acc_ambassador', 'Marks the rank as an ambassador for ambassador-only tools and visuals.'), +('acc_debug', 'Allows using debug-only features, commands, or internal tooling.'), +('acc_chat_no_limit', 'Lets the user hear and be heard regardless of room hearing distance limits.'), +('acc_chat_no_filter', 'Bypasses the word filter for chat and staff-generated messages.'), +('acc_nomute', 'Prevents the user from being muted by normal mute checks.'), +('acc_guild_admin', 'Allows bypassing guild admin restrictions when managing guilds.'), +('acc_catalog_ids', 'Allows seeing internal catalogue page ids, offer ids, or related technical catalogue identifiers.'), +('acc_modtool_ticket_q', 'Allows seeing and handling the moderation ticket queue.'), +('acc_modtool_user_logs', 'Allows reading user chat logs in the moderation tool.'), +('acc_modtool_user_alert', 'Allows sending moderation alerts or cautions to users.'), +('acc_modtool_user_kick', 'Allows kicking users from the moderation tool.'), +('acc_modtool_user_ban', 'Allows banning users from the moderation tool.'), +('acc_modtool_room_info', 'Allows viewing room information in the moderation tool.'), +('acc_modtool_room_logs', 'Allows viewing room chat logs in the moderation tool.'), +('acc_trade_anywhere', 'Allows starting trades outside the normal trade-enabled areas.'), +('acc_update_notifications', 'Allows receiving update notifications emitted by the emulator.'), +('acc_helper_use_guide_tool', 'Allows opening the helper guide tool.'), +('acc_helper_give_guide_tours', 'Allows accepting and handling guide tour requests.'), +('acc_helper_judge_chat_reviews', 'Allows reviewing helper or chat review tickets.'), +('acc_floorplan_editor', 'Allows opening and saving the floorplan editor.'), +('acc_camera', 'Allows using the in-room camera feature and related camera UI actions.'), +('acc_ads_background', 'Allows editing room advertisement backgrounds.'), +('cmd_wordquiz', 'Legacy alias of cmd_word_quiz for starting a room word-quiz event.'), +('acc_room_staff_tags', 'Shows staff tags or markers above the user while inside rooms.'), +('acc_infinite_friends', 'Removes the normal friend-list size limit.'), +('acc_mimic_unredeemed', 'Allows mimicking looks even when they contain unreleased or restricted clothing.'), +('cmd_update_youtube_playlists', 'Allows reloading YouTube playlist configuration for furniture integrations.'), +('cmd_add_youtube_playlist', 'Allows adding a new YouTube playlist entry.'), +('acc_mention', 'Allows using mention-related chat features beyond the normal rank restriction.'), +('cmd_setstate', 'Legacy room-editor permission for :setstate / :ss, used to change the selected furni state or extradata value.'), +('cmd_buildheight', 'Legacy room-editor permission for :buildheight / :bh, used to change the room build-height override.'), +('cmd_setrotation', 'Legacy room-editor permission for :setrotation / :rot, used to change the rotation of the selected furni.'), +('cmd_sellroom', 'Allows putting the current room up for sale through the sell-room command.'), +('cmd_buyroom', 'Allows purchasing a room that is marked as for sale through the buy-room command.'), +('cmd_pay', 'Allows transferring currency to another user through the pay command.'), +('cmd_kill', 'Allows using the kill command effect exposed by the current command set.'), +('cmd_hoverboard', 'Allows toggling the hoverboard effect or hoverboard movement mode.'), +('cmd_kiss', 'Allows using the kiss interaction command on another user.'), +('cmd_hug', 'Allows using the hug interaction command on another user.'), +('cmd_welcome', 'Allows triggering the welcome command behavior defined by the current command set.'), +('cmd_disable_effects', 'Allows disabling active avatar effects through the disable-effects command.'), +('cmd_brb', 'Allows toggling the be-right-back status command.'), +('cmd_nuke', 'Allows using the nuke command exposed by the current command set.'), +('cmd_slime', 'Allows applying the slime command/effect exposed by the current command set.'), +('cmd_explain', 'Allows using the explain command to send the predefined explanation/help flow to users.'), +('cmd_closedice', 'Legacy essentials permission for :closedice, used to close dice items in the room or all dice at once.'), +('acc_closedice_room', 'Legacy companion permission used by older closed-dice room checks.'), +('cmd_set', 'Legacy essentials permission for :set / :changefurni, the generic furni editing command documented by :set info.'), +('cmd_furnidata', 'Allows viewing technical furnidata information in-game for selected furniture.'), +('kiss_cmd', 'Legacy alias used for the kiss command permission.'), +('acc_calendar_force', 'Allows claiming calendar rewards even when the normal day-difference timing check would block the claim.'), +('cmd_update_calendar', 'Allows using :update_calendar to reload calendar definitions and rewards.'), +('cmd_update_all', 'Allows using :update_all to reload all supported runtime data sets in one command.'), +('cms_dance', 'Legacy CMS-side permission kept for website integrations; no direct in-emulator command handler was found in the current tree.'), +('acc_catalogfurni', 'Allows using catalogue administration features related to furniture pages and offers.'), +('acc_unignorable', 'Prevents the account from being ignored by other users through the ignore system.'), +('cmd_update_chat_bubbles', 'Allows using :update_chat_bubbles to reload chat-bubble definitions and assets.'), +('cmd_calendar_staff', 'Allows the staff-only actions exposed by the calendar command flow.'); + +UPDATE `permission_definitions` pd +INNER JOIN `tmp_permission_comments` tc ON tc.`permission_key` = pd.`permission_key` +SET pd.`comment` = tc.`comment`; + +DROP TEMPORARY TABLE IF EXISTS `tmp_permission_comments`; + +DROP PROCEDURE IF EXISTS `refresh_permission_definition_values`; + +DELIMITER $$ +CREATE PROCEDURE `refresh_permission_definition_values`() +BEGIN + DECLARE done INT DEFAULT 0; + DECLARE current_rank_id INT; + DECLARE current_column_name VARCHAR(32); + DECLARE rank_cursor CURSOR FOR SELECT `id` FROM `permission_ranks` ORDER BY `id` ASC; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; + + OPEN rank_cursor; + + rank_loop: LOOP + FETCH rank_cursor INTO current_rank_id; + + IF done = 1 THEN + LEAVE rank_loop; + END IF; + + SET current_column_name = CONCAT('rank_', current_rank_id); + + SELECT GROUP_CONCAT( + CONCAT( + 'SELECT ''', + REPLACE(`column_name`, '''', ''''''), + ''' AS permission_key, CAST(COALESCE(`', + REPLACE(`column_name`, '`', '``'), + '`, ''0'') AS UNSIGNED) AS permission_value FROM `permissions` WHERE `id` = ', + current_rank_id + ) + ORDER BY `ordinal_position` + SEPARATOR ' UNION ALL ' + ) INTO @permission_rank_source_sql + FROM `information_schema`.`columns` + WHERE `table_schema` = DATABASE() + AND `table_name` = 'permissions' + AND `column_name` NOT IN ( + 'id', + 'rank_name', + 'hidden_rank', + 'badge', + 'job_description', + 'staff_color', + 'staff_background', + 'level', + 'room_effect', + 'log_commands', + 'prefix', + 'prefix_color', + 'auto_credits_amount', + 'auto_pixels_amount', + 'auto_gotw_amount', + 'auto_points_amount' + ); + + SET @permission_rank_update_sql = CONCAT( + 'UPDATE `permission_definitions` pd ', + 'INNER JOIN (', + @permission_rank_source_sql, + ') src ON src.permission_key = pd.permission_key ', + 'SET pd.`', + current_column_name, + '` = src.permission_value' + ); + + PREPARE permission_rank_update_stmt FROM @permission_rank_update_sql; + EXECUTE permission_rank_update_stmt; + DEALLOCATE PREPARE permission_rank_update_stmt; + END LOOP; + + CLOSE rank_cursor; +END$$ +DELIMITER ; + +CALL `refresh_permission_definition_values`(); + + +CREATE TABLE IF NOT EXISTS `room_wired_settings` ( + `room_id` int(11) NOT NULL, + `inspect_mask` int(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can open and inspect Wired in the room. 1=everyone, 2=users with rights, 4=group members, 8=group admins.', + `modify_mask` int(11) NOT NULL DEFAULT 0 COMMENT 'Bitmask for who can modify Wired in the room. 2=users with rights, 4=group members, 8=group admins.', + PRIMARY KEY (`room_id`), + CONSTRAINT `fk_room_wired_settings_room_id` FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `room_user_wired_variables` ( + `room_id` int(11) NOT NULL, + `user_id` int(11) NOT NULL, + `variable_item_id` int(11) NOT NULL, + `value` int(11) DEFAULT NULL, + `created_at` int(11) NOT NULL DEFAULT 0, + `updated_at` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`room_id`, `user_id`, `variable_item_id`), + KEY `idx_room_user_wired_variables_room_item` (`room_id`, `variable_item_id`), + KEY `idx_room_user_wired_variables_user` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `room_furni_wired_variables` ( + `room_id` int(11) NOT NULL, + `furni_id` int(11) NOT NULL, + `variable_item_id` int(11) NOT NULL, + `value` int(11) DEFAULT NULL, + `created_at` int(11) NOT NULL DEFAULT 0, + `updated_at` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`room_id`, `furni_id`, `variable_item_id`), + KEY `idx_room_furni_wired_variables_room_item` (`room_id`, `variable_item_id`), + KEY `idx_room_furni_wired_variables_furni` (`furni_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `room_wired_variables` ( + `room_id` int(11) NOT NULL, + `variable_item_id` int(11) NOT NULL, + `value` int(11) NOT NULL DEFAULT 0, + `created_at` int(11) NOT NULL DEFAULT 0, + `updated_at` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`room_id`, `variable_item_id`), + KEY `idx_room_wired_variables_room_item` (`room_id`, `variable_item_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +ALTER TABLE `room_user_wired_variables` + ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`; + +ALTER TABLE `room_user_wired_variables` + ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`; + +UPDATE `room_user_wired_variables` +SET + `created_at` = IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()), + `updated_at` = IF(`updated_at` > 0, `updated_at`, IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP())); + +ALTER TABLE `room_furni_wired_variables` + ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`; + +ALTER TABLE `room_furni_wired_variables` + ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`; + +UPDATE `room_furni_wired_variables` +SET + `created_at` = IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP()), + `updated_at` = IF(`updated_at` > 0, `updated_at`, IF(`created_at` > 0, `created_at`, UNIX_TIMESTAMP())); + +ALTER TABLE `room_wired_variables` + ADD COLUMN IF NOT EXISTS `created_at` int(11) NOT NULL DEFAULT 0 AFTER `value`; + +ALTER TABLE `room_wired_variables` + ADD COLUMN IF NOT EXISTS `updated_at` int(11) NOT NULL DEFAULT 0 AFTER `created_at`; + +UPDATE `room_wired_variables` +SET + `created_at` = 0, + `updated_at` = IF(`updated_at` > 0, `updated_at`, UNIX_TIMESTAMP()); + +INSERT INTO `chat_bubbles` (`type`, `name`, `permission`, `overridable`, `triggers_talking_furniture`) VALUES +(200, 'SHOW_MESSAGE_RED', '', 1, 0), +(201, 'SHOW_MESSAGE_GREEN', '', 1, 0), +(202, 'SHOW_MESSAGE_BLUE', '', 1, 0), +(210, 'SHOW_MESSAGE_ALERT', '', 1, 0), +(211, 'SHOW_MESSAGE_INFO', '', 1, 0), +(212, 'SHOW_MESSAGE_WARNING', '', 1, 0), +(220, 'SHOW_MESSAGE_WRONG', '', 1, 0), +(221, 'SHOW_MESSAGE_WRONG_CIRCLED', '', 1, 0), +(222, 'SHOW_MESSAGE_CORRECT', '', 1, 0), +(223, 'SHOW_MESSAGE_CORRECT_CIRCLED', '', 1, 0), +(224, 'SHOW_MESSAGE_QUESTION', '', 1, 0), +(225, 'SHOW_MESSAGE_QUESTION_CIRCLED', '', 1, 0), +(226, 'SHOW_MESSAGE_ARROW_UP', '', 1, 0), +(227, 'SHOW_MESSAGE_ARROW_UP_CIRCLED', '', 1, 0), +(228, 'SHOW_MESSAGE_ARROW_DOWN', '', 1, 0), +(229, 'SHOW_MESSAGE_ARROW_DOWN_CIRCLED', '', 1, 0), +(250, 'SHOW_MESSAGE_SKULL', '', 1, 0), +(251, 'SHOW_MESSAGE_SKULL_ALT', '', 1, 0), +(252, 'SHOW_MESSAGE_MAGNIFIER', '', 1, 0) +ON DUPLICATE KEY UPDATE + `name` = VALUES(`name`), + `permission` = VALUES(`permission`), + `overridable` = VALUES(`overridable`), + `triggers_talking_furniture` = VALUES(`triggers_talking_furniture`); + +ALTER TABLE `catalog_club_offers` +MODIFY COLUMN `type` ENUM('HC', 'VIP', 'BUILDERS_CLUB', 'BUILDERS_CLUB_ADDON') NOT NULL DEFAULT 'HC'; + +ALTER TABLE `catalog_pages` + MODIFY COLUMN `page_layout` ENUM( + 'default_3x3', + 'club_buy', + 'club_gift', + 'frontpage', + 'spaces', + 'recycler', + 'recycler_info', + 'recycler_prizes', + 'trophies', + 'plasto', + 'marketplace', + 'marketplace_own_items', + 'spaces_new', + 'soundmachine', + 'guilds', + 'guild_furni', + 'info_duckets', + 'info_rentables', + 'info_pets', + 'roomads', + 'single_bundle', + 'sold_ltd_items', + 'badge_display', + 'bots', + 'pets', + 'pets2', + 'pets3', + 'productpage1', + 'room_bundle', + 'recent_purchases', + 'default_3x3_color_grouping', + 'guild_forum', + 'vip_buy', + 'info_loyalty', + 'loyalty_vip_buy', + 'collectibles', + 'petcustomization', + 'frontpage_featured', + 'builders_club_frontpage', + 'builders_club_addons', + 'builders_club_loyalty' + ) NOT NULL DEFAULT 'default_3x3'; + +ALTER TABLE `catalog_pages` +ADD COLUMN IF NOT EXISTS `catalog_mode` ENUM('NORMAL', 'BUILDER', 'BOTH') NOT NULL DEFAULT 'NORMAL' AFTER `club_only`; + +ALTER TABLE `rooms` + ADD COLUMN IF NOT EXISTS `builders_club_trial_locked` TINYINT(1) NOT NULL DEFAULT 0 AFTER `allow_underpass`, + ADD COLUMN IF NOT EXISTS `builders_club_original_state` VARCHAR(16) NOT NULL DEFAULT 'open' AFTER `builders_club_trial_locked`; + +CREATE TABLE IF NOT EXISTS `builders_club_items` ( + `item_id` INT(11) NOT NULL, + `user_id` INT(11) NOT NULL, + `room_id` INT(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`item_id`), + KEY `idx_builders_club_items_user_id` (`user_id`), + KEY `idx_builders_club_items_room_id` (`room_id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; + +ALTER TABLE `catalog_pages_bc` + MODIFY COLUMN `page_layout` ENUM( + 'default_3x3', + 'club_buy', + 'club_gift', + 'frontpage', + 'spaces', + 'recycler', + 'recycler_info', + 'recycler_prizes', + 'trophies', + 'plasto', + 'marketplace', + 'marketplace_own_items', + 'spaces_new', + 'soundmachine', + 'guilds', + 'guild_furni', + 'info_duckets', + 'info_rentables', + 'info_pets', + 'roomads', + 'single_bundle', + 'sold_ltd_items', + 'badge_display', + 'bots', + 'pets', + 'pets2', + 'pets3', + 'productpage1', + 'room_bundle', + 'recent_purchases', + 'default_3x3_color_grouping', + 'guild_forum', + 'vip_buy', + 'info_loyalty', + 'loyalty_vip_buy', + 'collectibles', + 'petcustomization', + 'frontpage_featured', + 'builders_club_frontpage', + 'builders_club_addons', + 'builders_club_loyalty' + ) NOT NULL DEFAULT 'default_3x3'; + diff --git a/Database Updates/07012026_UpdateDatabase_to_4-0-1.sql b/Database Updates/07012026_UpdateDatabase_to_4-0-1.sql deleted file mode 100644 index 97072873..00000000 --- a/Database Updates/07012026_UpdateDatabase_to_4-0-1.sql +++ /dev/null @@ -1,152 +0,0 @@ --- Wired Abuse Protection Settings --- These settings control the wired abuse detection and rate limiting system - --- Maximum recursion depth to prevent infinite loops (e.g., collision + chase triggering each other) -INSERT INTO `emulator_settings` (`key`, `value`) VALUES ('wired.abuse.max.recursion.depth', '10'); - --- Maximum events of same type per room within the rate limit window before triggering a ban --- Set higher for rooms with many users and complex wired setups -INSERT INTO `emulator_settings` (`key`, `value`) VALUES ('wired.abuse.max.events.per.window', '100'); - --- Time window in milliseconds for counting rapid events --- Events are counted within this window to detect abuse patterns -INSERT INTO `emulator_settings` (`key`, `value`) VALUES ('wired.abuse.rate.limit.window.ms', '10000'); - --- Duration in milliseconds to ban wired execution in a room after abuse is detected --- Default: 600000 (10 minutes) -INSERT INTO `emulator_settings` (`key`, `value`) VALUES ('wired.abuse.ban.duration.ms', '600000'); - --- Wired Abuse Alert Texts --- Alert shown to all users in the room when wired is temporarily disabled -INSERT INTO `emulator_texts` (`key`, `value`) VALUES ('wired.abuse.room.alert', 'Wired execution has been temporarily disabled in this room due to abuse detection. It will resume in %minutes% minutes.'); - --- Title for the staff bubble alert -INSERT INTO `emulator_texts` (`key`, `value`) VALUES ('wired.abuse.staff.title', 'Wired Abuse Detected'); - --- Message for the staff bubble alert --- Available placeholders: %roomname%, %owner%, %minutes% -INSERT INTO `emulator_texts` (`key`, `value`) VALUES ('wired.abuse.staff.message', 'Room: %roomname%\nOwner: %owner%\nBanned for %minutes% minutes.'); - --- Link text for the staff bubble alert (navigates to the room) -INSERT INTO `emulator_texts` (`key`, `value`) VALUES ('wired.abuse.staff.link', 'Go to Room'); - --- Default tick resolution for wired timer triggers (in milliseconds) -INSERT INTO `emulator_settings` (`key`, `value`) VALUES ('wired.tick.resolution', '100'); - - --- ===================================================== --- Wired Engine Rewrite - Configuration Settings --- ===================================================== --- This SQL script adds the configuration options for the new --- context-driven wired engine architecture. --- --- Run this script after upgrading to enable the new wired system. --- ===================================================== - --- Insert new wired engine configuration settings -INSERT INTO `emulator_settings` (`key`, `value`) VALUES -('wired.engine.enabled', '0'), -('wired.engine.exclusive', '0'), -('wired.engine.maxStepsPerStack', '100'), -('wired.engine.debug', '0') -ON DUPLICATE KEY UPDATE `key` = `key`; - --- ===================================================== --- Configuration Options Explained: --- ===================================================== --- --- wired.engine.enabled --- Enable the new wired engine. When set to 1, the new engine --- runs alongside the legacy WiredHandler for parallel testing. --- Default: 0 (disabled) --- --- wired.engine.exclusive --- When set to 1, disables the legacy WiredHandler completely. --- Only enable this after thorough testing with parallel mode. --- Default: 0 (legacy handler still active) --- --- wired.engine.maxStepsPerStack --- Maximum number of steps (trigger checks + condition evaluations --- + effect executions) allowed per wired stack execution. --- Prevents infinite loops from misconfigured wired setups. --- Default: 100 --- --- wired.engine.debug --- Enable verbose debug logging for wired execution. --- Useful for troubleshooting wired stack behavior. --- Default: 0 (disabled) --- --- ===================================================== --- Migration Path: --- ===================================================== --- --- Phase 1: Parallel Testing --- UPDATE emulator_settings SET value = '1' WHERE `key` = 'wired.engine.enabled'; --- -- Test all wired functionality, compare behavior between old and new --- --- Phase 2: Switch to New Engine --- UPDATE emulator_settings SET value = '1' WHERE `key` = 'wired.engine.exclusive'; --- -- Legacy handler disabled, new engine handles all wired events --- --- Phase 3: Cleanup (after confirming stability) --- -- Remove legacy WiredHandler calls from codebase --- --- ===================================================== - - --- ===================================================== --- Wired Tick System - Configuration Settings --- ===================================================== --- This SQL script adds the configuration options for the new --- high-resolution wired tick system (50ms default). --- --- Run this script to configure the wired timer triggers. --- ===================================================== - --- Insert new wired tick system configuration settings -INSERT INTO `emulator_settings` (`key`, `value`) VALUES -('wired.tick.interval.ms', '50'), -('wired.tick.debug', '0'), -('wired.tick.thread.priority', '6') -ON DUPLICATE KEY UPDATE `key` = `key`; - --- ===================================================== --- Configuration Options Explained: --- ===================================================== --- --- wired.tick.interval.ms --- The tick interval in milliseconds for wired timer triggers. --- Lower values = more precise timing but higher CPU usage. --- Recommended: 50 (default), Range: 10-500 --- Default: 50 --- --- wired.tick.debug --- Enable verbose debug logging for wired tick operations. --- Logs each tick cycle and trigger execution. --- Warning: Very verbose, only enable for troubleshooting. --- Default: 0 (disabled) --- --- wired.tick.thread.priority --- Thread priority for the wired tick service (1-10). --- Higher priority = better timing accuracy under load. --- Java Thread priorities: MIN=1, NORM=5, MAX=10 --- Default: 6 (slightly above normal) --- --- ===================================================== --- Usage Examples: --- ===================================================== --- --- Increase tick resolution for competitive mini-games: --- UPDATE emulator_settings SET value = '25' WHERE `key` = 'wired.tick.interval.ms'; --- --- Reduce CPU usage on low-end servers: --- UPDATE emulator_settings SET value = '100' WHERE `key` = 'wired.tick.interval.ms'; --- --- Enable debug logging for troubleshooting: --- UPDATE emulator_settings SET value = '1' WHERE `key` = 'wired.tick.debug'; --- --- ===================================================== -INSERT INTO `emulator_settings` (`key`, `value`) VALUES ('pathfinder.click.delay', '0'); -INSERT INTO `emulator_settings` (`key`, `value`) VALUES ('pathfinder.retro-style.diagonals', '0'); -INSERT INTO `emulator_settings` (`key`, `value`) VALUES ('pathfinder.step.allow.falling', '1'); - diff --git a/Database Updates/09012026_UpdateDatabase_to_4-0-2.sql b/Database Updates/09012026_UpdateDatabase_to_4-0-2.sql deleted file mode 100644 index 9d094a91..00000000 --- a/Database Updates/09012026_UpdateDatabase_to_4-0-2.sql +++ /dev/null @@ -1,672 +0,0 @@ -SET NAMES utf8mb4; -SET FOREIGN_KEY_CHECKS = 0; - --- ===================================================== --- SECTION 1: Pet System Emulator Settings --- ===================================================== - -INSERT INTO `emulator_settings` (`key`, `value`) VALUES --- Core pet limits -('hotel.pets.max.room', '15'), -('hotel.pets.max.inventory', '25'), -('hotel.pets.name.length.min', '1'), -('hotel.pets.name.length.max', '15'), -('hotel.daily.respect.pets', '3'), - --- Command cooldown and spam prevention -('pet.command.cooldown_ms', '2000'), -('pet.command.max_same_spam', '3'), -('pet.command.spam_reset_ms', '10000'), -('pet.command.min_energy', '15'), -('pet.command.min_happiness', '10'), -('pet.command.base_obey_chance', '70'), - --- Pet behavior settings -('pet.behavior.autonomous_action_delay', '5000'), -('pet.behavior.idle_wander_min_ms', '10000'), -('pet.behavior.idle_wander_max_ms', '30000'), - --- Pet stats decay/recovery rates (per cycle) -('pet.stats.hunger_decay', '1'), -('pet.stats.thirst_decay', '1'), -('pet.stats.energy_decay', '1'), -('pet.stats.happiness_decay', '1'), -('pet.stats.energy_recovery', '5'), -('pet.stats.happiness_recovery', '1'), - --- Pet thresholds (below this = needs attention) -('pet.threshold.hungry', '50'), -('pet.threshold.thirsty', '50'), -('pet.threshold.tired', '30'), -('pet.threshold.sad', '30'), - --- Pet breeding -('pet.breeding.timeout_seconds', '120') -ON DUPLICATE KEY UPDATE `value` = VALUES(`value`); - --- ===================================================== --- SECTION 2: Pet Actions (Pet Type Definitions) --- ===================================================== - -DROP TABLE IF EXISTS `pet_actions`; -CREATE TABLE `pet_actions` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `pet_type` int(11) NOT NULL, - `pet_name` varchar(32) NOT NULL DEFAULT '', - `offspring_type` int(11) NOT NULL DEFAULT -1, - `happy_actions` varchar(100) NOT NULL DEFAULT 'sml', - `tired_actions` varchar(100) NOT NULL DEFAULT 'trd', - `random_actions` varchar(100) NOT NULL DEFAULT 'lov', - `can_swim` enum('0','1') NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - UNIQUE KEY `pet_type` (`pet_type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -INSERT INTO `pet_actions` (`pet_type`, `pet_name`, `offspring_type`, `happy_actions`, `tired_actions`, `random_actions`, `can_swim`) VALUES -(0, 'Dog', 29, 'sml,wav,joy', 'trd,yng', 'lov,snf', '0'), -(1, 'Cat', 28, 'sml,pur', 'trd,yng', 'lov,lck', '0'), -(2, 'Crocodile', -1, 'sml', 'trd', 'lov,snp', '1'), -(3, 'Terrier', 25, 'sml,wav,joy', 'trd,yng', 'lov,snf', '0'), -(4, 'Bear', 24, 'sml,grw', 'trd,yng', 'lov', '0'), -(5, 'Pig', 30, 'sml,oink', 'trd,yng', 'lov,rol', '0'), -(6, 'Lion', -1, 'sml,ror', 'trd,yng', 'lov', '0'), -(7, 'Rhino', -1, 'sml', 'trd,yng', 'lov', '0'), -(8, 'Tarantula', -1, 'sml', 'trd', 'lov,crw', '0'), -(9, 'Turtle', -1, 'sml', 'trd', 'lov', '1'), -(10, 'Chick', -1, 'sml,chp', 'trd', 'lov,pck', '0'), -(11, 'Frog', -1, 'sml,crk', 'trd', 'lov,jmp', '1'), -(12, 'Dragon', -1, 'sml,flm', 'trd,smk', 'lov,fly', '0'), -(13, 'Monster', -1, 'sml', 'trd', 'lov', '0'), -(14, 'Monkey', -1, 'sml,ook', 'trd,yng', 'lov,swg', '0'), -(15, 'Horse', -1, 'sml,nei', 'trd,yng', 'lov', '0'), -(16, 'Monsterplant', -1, 'sml', 'trd', 'lov', '0'), -(17, 'Bunny', -1, 'sml,hop', 'trd,yng', 'lov', '0'), -(18, 'Evil Bunny', -1, 'sml', 'trd', 'lov', '0'), -(19, 'Bored Bunny', -1, 'sml', 'trd', 'lov', '0'), -(20, 'Cute Bunny', -1, 'sml,hop', 'trd', 'lov', '0'), -(21, 'Wise Pigeon', -1, 'sml,coo', 'trd', 'lov,pck', '0'), -(22, 'Evil Pigeon', -1, 'sml', 'trd', 'lov,pck', '0'), -(23, 'Evil Monkey', -1, 'sml', 'trd', 'lov,swg', '0'), -(24, 'Baby Bear', -1, 'sml', 'trd,yng', 'lov', '0'), -(25, 'Baby Terrier', -1, 'sml', 'trd,yng', 'lov', '0'), -(26, 'Gnome', -1, 'sml,grn', 'trd', 'lov', '0'), -(27, 'Leprechaun', -1, 'sml,grn', 'trd', 'lov,jig', '0'), -(28, 'Baby Cat', -1, 'sml', 'trd,yng', 'lov', '0'), -(29, 'Baby Dog', -1, 'sml', 'trd,yng', 'lov', '0'), -(30, 'Baby Pig', -1, 'sml', 'trd,yng', 'lov', '0'), -(31, 'Haloompa', -1, 'sml', 'trd', 'lov', '0'), -(32, 'Fools Pet', -1, 'sml', 'trd', 'lov', '0'), -(33, 'Pterodactyl', -1, 'sml,sqk', 'trd', 'lov,fly', '0'), -(34, 'Velociraptor', -1, 'sml,hss', 'trd', 'lov,clw', '0'), -(35, 'Cow', -1, 'sml,moo', 'trd,yng', 'lov,chw', '0'); - --- ===================================================== --- SECTION 3: Pet Commands Data (English Command Names) --- ===================================================== --- Command IDs mapped to PetManager.petActions: --- 0=Free, 1=Sit, 2=Down, 3=Here, 4=Beg, 5=PlayDead, 6=Stay, 7=Follow --- 8=Stand, 9=Jump, 10=Speak, 11=Play, 12=Silent, 13=Nest, 14=Drink --- 15=FollowLeft, 16=FollowRight, 17=PlayFootball, 18=Teleport, 19=Bounce --- 20=Flatten, 21=Dance, 22=Spin, 23=Switch, 24=MoveForward --- 25=TurnLeft, 26=TurnRight, 27=Relax, 28=Croak, 29=Dip, 30=Wave --- 31=Mambo, 32=HighJump, 33=ChickenDance, 34=TripleJump --- 35=Wings, 36=BreatheFire, 37=Hang, 38=Torch, 40=Swing, 41=Roll --- 42=RingOfFire, 43=Eat, 44=WagTail, 45=Count, 46=Breed - -DROP TABLE IF EXISTS `pet_commands_data`; -CREATE TABLE `pet_commands_data` ( - `command_id` int(11) NOT NULL, - `text` varchar(25) NOT NULL, - `required_level` int(11) NOT NULL DEFAULT 1, - `reward_xp` int(11) NOT NULL DEFAULT 5, - `cost_happiness` int(11) NOT NULL DEFAULT 0, - `cost_energy` int(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`command_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - -INSERT INTO `pet_commands_data` (`command_id`, `text`, `required_level`, `reward_xp`, `cost_happiness`, `cost_energy`) VALUES -(0, 'free', 1, 5, 0, 0), -(1, 'sit', 1, 5, 2, 2), -(2, 'down', 2, 10, 3, 3), -(3, 'come here', 2, 10, 2, 5), -(4, 'beg', 2, 10, 3, 4), -(5, 'play dead', 3, 15, 4, 5), -(6, 'stay', 4, 10, 2, 3), -(7, 'follow', 5, 15, 3, 8), -(8, 'stand', 6, 15, 2, 3), -(9, 'jump', 6, 15, 4, 8), -(10, 'speak', 7, 10, 3, 3), -(11, 'play', 8, 5, 5, 10), -(12, 'silent', 8, 5, 2, 1), -(13, 'nest', 5, 5, 0, 0), -(14, 'drink', 1, 5, 0, 0), -(15, 'follow left', 15, 15, 4, 10), -(16, 'follow right', 15, 15, 4, 10), -(17, 'play football', 10, 5, 5, 12), -(18, 'teleport', 9, 5, 3, 5), -(19, 'bounce', 9, 5, 5, 10), -(20, 'flatten', 11, 5, 3, 4), -(21, 'dance', 12, 10, 6, 12), -(22, 'spin', 10, 5, 4, 8), -(23, 'switch', 12, 5, 3, 3), -(24, 'move forward', 17, 5, 2, 2), -(25, 'turn left', 18, 5, 2, 2), -(26, 'turn right', 18, 5, 2, 2), -(27, 'relax', 13, 5, 0, 0), -(28, 'croak', 14, 5, 3, 3), -(29, 'dip', 14, 5, 5, 10), -(30, 'wave', 5, 5, 2, 3), -(31, 'mambo', 18, 5, 6, 12), -(32, 'high jump', 18, 5, 5, 12), -(33, 'chicken dance', 7, 5, 5, 10), -(34, 'triple jump', 9, 5, 6, 15), -(35, 'spread wings', 8, 5, 4, 6), -(36, 'breathe fire', 10, 5, 5, 8), -(37, 'hang', 12, 5, 4, 6), -(38, 'torch', 6, 5, 3, 5), -(40, 'swing', 13, 5, 4, 8), -(41, 'roll', 10, 5, 5, 10), -(42, 'ring of fire', 20, 10, 8, 15), -(43, 'eat', 1, 5, 0, 0), -(44, 'wag tail', 4, 5, 3, 4), -(45, 'count', 6, 5, 4, 5), -(46, 'breed', 1, 5, 10, 20); - --- ===================================================== --- SECTION 4: Pet Commands (Pet Type -> Command Mapping) --- ===================================================== - -DROP TABLE IF EXISTS `pet_commands`; -CREATE TABLE `pet_commands` ( - `pet_id` int(11) NOT NULL, - `command_id` int(11) NOT NULL, - PRIMARY KEY (`pet_id`, `command_id`), - KEY `pet_id` (`pet_id`), - KEY `command_id` (`command_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- Dog (0) - Full standard pet commands -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9), -(0, 10), (0, 11), (0, 12), (0, 13), (0, 14), (0, 15), (0, 16), (0, 17), (0, 24), -(0, 25), (0, 26), (0, 43), (0, 44), (0, 46); - --- Cat (1) - Full standard pet commands + breed -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), -(1, 10), (1, 11), (1, 12), (1, 13), (1, 14), (1, 15), (1, 16), (1, 17), (1, 24), -(1, 25), (1, 26), (1, 43), (1, 46); - --- Crocodile (2) - Standard commands (can swim) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9), -(2, 10), (2, 11), (2, 12), (2, 13), (2, 14), (2, 15), (2, 16), (2, 17), (2, 24), -(2, 25), (2, 26), (2, 29), (2, 43); - --- Terrier (3) - Standard commands + breed -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), -(3, 10), (3, 11), (3, 12), (3, 13), (3, 14), (3, 15), (3, 16), (3, 17), (3, 24), -(3, 25), (3, 26), (3, 43), (3, 46); - --- Bear (4) - Standard commands + breed -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(4, 0), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (4, 6), (4, 7), (4, 8), (4, 9), -(4, 10), (4, 11), (4, 12), (4, 13), (4, 14), (4, 15), (4, 16), (4, 17), (4, 24), -(4, 25), (4, 26), (4, 43), (4, 46); - --- Pig (5) - Standard commands + breed -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(5, 0), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), (5, 6), (5, 7), (5, 8), (5, 9), -(5, 10), (5, 11), (5, 12), (5, 13), (5, 14), (5, 15), (5, 16), (5, 17), (5, 24), -(5, 25), (5, 26), (5, 43), (5, 46); - --- Lion (6) - Standard commands -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(6, 0), (6, 1), (6, 2), (6, 3), (6, 4), (6, 5), (6, 6), (6, 7), (6, 8), (6, 9), -(6, 10), (6, 11), (6, 12), (6, 13), (6, 14), (6, 15), (6, 16), (6, 17), (6, 24), -(6, 25), (6, 26), (6, 43); - --- Rhino (7) - Standard commands -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(7, 0), (7, 1), (7, 2), (7, 3), (7, 4), (7, 5), (7, 6), (7, 7), (7, 8), (7, 9), -(7, 10), (7, 11), (7, 12), (7, 13), (7, 14), (7, 15), (7, 16), (7, 17), (7, 24), -(7, 25), (7, 26), (7, 43); - --- Tarantula (8) - Spider commands (bounce, flatten, spin, etc.) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(8, 0), (8, 2), (8, 3), (8, 5), (8, 6), (8, 7), (8, 9), (8, 10), (8, 11), (8, 13), -(8, 14), (8, 15), (8, 16), (8, 17), (8, 19), (8, 20), (8, 21), (8, 22), (8, 23), -(8, 24), (8, 25), (8, 26), (8, 43); - --- Turtle (9) - Aquatic commands -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(9, 0), (9, 1), (9, 2), (9, 3), (9, 6), (9, 7), (9, 8), (9, 10), (9, 11), (9, 13), -(9, 14), (9, 15), (9, 16), (9, 24), (9, 25), (9, 26), (9, 29), (9, 41), (9, 43); - --- Chick (10) - Bird commands -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(10, 0), (10, 2), (10, 3), (10, 6), (10, 7), (10, 11), (10, 13), (10, 15), (10, 16), -(10, 17), (10, 33); - --- Frog (11) - Amphibian commands (croak, dip, wave, mambo) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(11, 0), (11, 1), (11, 2), (11, 3), (11, 4), (11, 5), (11, 6), (11, 7), (11, 9), -(11, 13), (11, 14), (11, 15), (11, 16), (11, 17), (11, 27), (11, 28), (11, 29), -(11, 30), (11, 31), (11, 43); - --- Dragon (12) - Dragon special commands (fire, hang, swing, ring of fire) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(12, 0), (12, 2), (12, 3), (12, 5), (12, 6), (12, 7), (12, 8), (12, 9), (12, 10), -(12, 11), (12, 12), (12, 13), (12, 14), (12, 15), (12, 16), (12, 22), (12, 35), -(12, 36), (12, 37), (12, 38), (12, 40), (12, 41), (12, 42), (12, 43); - --- Monster (13) - Basic commands -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(13, 0), (13, 2), (13, 3), (13, 6), (13, 7), (13, 13); - --- Monkey (14) - Monkey commands (wave, hang, swing) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(14, 0), (14, 1), (14, 2), (14, 3), (14, 4), (14, 5), (14, 6), (14, 7), (14, 9), -(14, 13), (14, 14), (14, 15), (14, 16), (14, 17), (14, 27), (14, 29), (14, 30), -(14, 31), (14, 37), (14, 40), (14, 43); - --- Horse (15) - Rideable pet commands + wag tail, count -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(15, 0), (15, 2), (15, 3), (15, 6), (15, 7), (15, 10), (15, 11), (15, 12), (15, 13), -(15, 14), (15, 15), (15, 16), (15, 24), (15, 25), (15, 26), (15, 43), (15, 44), (15, 45); - --- Monsterplant (16) - Minimal commands -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(16, 0), (16, 14), (16, 43); - --- Bunnies (17-20) - Bunny commands -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(17, 0), (17, 2), (17, 3), (17, 6), (17, 7), (17, 11), (17, 13), (17, 15), (17, 16), (17, 17), -(18, 0), (18, 2), (18, 3), (18, 6), (18, 7), (18, 11), (18, 13), (18, 15), (18, 16), (18, 17), -(19, 0), (19, 2), (19, 3), (19, 6), (19, 7), (19, 11), (19, 13), (19, 15), (19, 16), (19, 17), -(20, 0), (20, 2), (20, 3), (20, 6), (20, 7), (20, 11), (20, 13), (20, 15), (20, 16), (20, 17); - --- Pigeons (21-22) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(21, 0), (21, 2), (21, 3), (21, 6), (21, 7), (21, 11), (21, 13), (21, 15), (21, 16), (21, 17), -(22, 0), (22, 2), (22, 3), (22, 6), (22, 7), (22, 11), (22, 13), (22, 15), (22, 16), (22, 17); - --- Evil Monkey (23) - Monkey commands -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(23, 0), (23, 1), (23, 2), (23, 3), (23, 4), (23, 5), (23, 6), (23, 7), (23, 9), -(23, 13), (23, 14), (23, 15), (23, 16), (23, 17), (23, 25), (23, 26), (23, 27), -(23, 29), (23, 30), (23, 31), (23, 37), (23, 40), (23, 43); - --- Baby Bear (24) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(24, 0), (24, 1), (24, 2), (24, 3), (24, 4), (24, 6), (24, 7), (24, 8), (24, 10), -(24, 11), (24, 12), (24, 13), (24, 14), (24, 15), (24, 16), (24, 17), (24, 24), -(24, 25), (24, 26), (24, 43); - --- Baby Terrier (25) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(25, 0), (25, 1), (25, 2), (25, 3), (25, 4), (25, 6), (25, 7), (25, 8), (25, 10), -(25, 11), (25, 12), (25, 13), (25, 14), (25, 15), (25, 16), (25, 17), (25, 24), -(25, 25), (25, 26), (25, 43); - --- Gnome (26) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(26, 0), (26, 1), (26, 2), (26, 3), (26, 4), (26, 6), (26, 7), (26, 8), (26, 13), -(26, 14), (26, 15), (26, 16), (26, 17), (26, 25), (26, 26), (26, 43); - --- Leprechaun (27) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(27, 0), (27, 1), (27, 2), (27, 3), (27, 4), (27, 6), (27, 7), (27, 8), (27, 13), -(27, 14), (27, 15), (27, 16), (27, 17), (27, 25), (27, 26), (27, 43); - --- Baby Cat (28) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(28, 0), (28, 1), (28, 2), (28, 3), (28, 4), (28, 6), (28, 7), (28, 8), (28, 10), -(28, 11), (28, 12), (28, 13), (28, 14), (28, 15), (28, 16), (28, 17), (28, 24), -(28, 25), (28, 26), (28, 43); - --- Baby Dog (29) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(29, 0), (29, 1), (29, 2), (29, 3), (29, 4), (29, 6), (29, 7), (29, 8), (29, 10), -(29, 11), (29, 12), (29, 13), (29, 14), (29, 15), (29, 16), (29, 17), (29, 24), -(29, 25), (29, 26), (29, 43); - --- Baby Pig (30) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(30, 0), (30, 1), (30, 2), (30, 3), (30, 4), (30, 6), (30, 7), (30, 8), (30, 10), -(30, 11), (30, 12), (30, 13), (30, 14), (30, 15), (30, 16), (30, 17), (30, 24), -(30, 25), (30, 26), (30, 43); - --- Haloompa (31) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(31, 0), (31, 1), (31, 2), (31, 3), (31, 4), (31, 6), (31, 7), (31, 8), (31, 13), -(31, 14), (31, 15), (31, 16), (31, 17), (31, 25), (31, 26), (31, 43); - --- Fools Pet (32) - Full dance/trick set -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(32, 0), (32, 1), (32, 2), (32, 3), (32, 4), (32, 5), (32, 6), (32, 7), (32, 8), -(32, 9), (32, 13), (32, 14), (32, 15), (32, 16), (32, 17), (32, 21), (32, 25), -(32, 26), (32, 43); - --- Pterodactyl (33) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(33, 0), (33, 2), (33, 3), (33, 4), (33, 6), (33, 7), (33, 11), (33, 13), (33, 14), -(33, 15), (33, 16), (33, 25), (33, 26), (33, 43); - --- Velociraptor (34) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(34, 0), (34, 1), (34, 2), (34, 3), (34, 6), (34, 7), (34, 8), (34, 10), (34, 12), -(34, 13), (34, 14), (34, 15), (34, 16), (34, 17), (34, 21), (34, 26), (34, 43); - --- Cow (35) -INSERT INTO `pet_commands` (`pet_id`, `command_id`) VALUES -(35, 0), (35, 2), (35, 3), (35, 4), (35, 6), (35, 7), (35, 13), (35, 14), (35, 15), -(35, 16), (35, 17), (35, 25), (35, 26), (35, 30), (35, 43); - --- ===================================================== --- SECTION 5: Pet Vocals (Pet Speech Messages) --- ===================================================== --- pet_id = -1 means general vocals for all pets --- pet_id >= 0 means specific to that pet type - -CREATE TABLE IF NOT EXISTS `pet_vocals` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `pet_id` int(11) NOT NULL DEFAULT -1, - `type` varchar(20) NOT NULL, - `message` varchar(255) NOT NULL, - PRIMARY KEY (`id`), - KEY `pet_id` (`pet_id`), - KEY `type` (`type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- Clear existing vocals -DELETE FROM `pet_vocals`; - --- ===================================================== --- GENERAL VOCALS (pet_id = -1, used by all pets) --- ===================================================== - --- GREET_OWNER - When owner enters room -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'GREET_OWNER', '*perks up excitedly*'), -(-1, 'GREET_OWNER', 'You''re back!'), -(-1, 'GREET_OWNER', '*bounces with joy*'), -(-1, 'GREET_OWNER', 'I missed you!'), -(-1, 'GREET_OWNER', '*runs in circles happily*'), -(-1, 'GREET_OWNER', 'Yay! My favorite person!'), -(-1, 'GREET_OWNER', '*jumps up and down*'), -(-1, 'GREET_OWNER', 'Finally! You''re here!'), -(-1, 'GREET_OWNER', '*tail wagging intensifies*'); - --- LEVEL_UP - When pet gains a level -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'LEVEL_UP', '*jumps with joy!*'), -(-1, 'LEVEL_UP', 'I leveled up!'), -(-1, 'LEVEL_UP', 'I feel stronger!'), -(-1, 'LEVEL_UP', 'Woohoo! New level!'), -(-1, 'LEVEL_UP', '*celebrates*'), -(-1, 'LEVEL_UP', 'I''m getting better!'), -(-1, 'LEVEL_UP', 'Level up! Yeah!'); - --- MUTED - When told to be silent -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'MUTED', '*stays quiet*'), -(-1, 'MUTED', '...'), -(-1, 'MUTED', '*zips lips*'), -(-1, 'MUTED', '*nods silently*'); - --- UNKNOWN_COMMAND - When pet doesn't understand -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'UNKNOWN_COMMAND', '*tilts head confused*'), -(-1, 'UNKNOWN_COMMAND', 'Huh?'), -(-1, 'UNKNOWN_COMMAND', 'I don''t understand...'), -(-1, 'UNKNOWN_COMMAND', '*looks puzzled*'), -(-1, 'UNKNOWN_COMMAND', 'What do you mean?'), -(-1, 'UNKNOWN_COMMAND', '*scratches head*'); - --- DISOBEY - When pet refuses command -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'DISOBEY', '*ignores command*'), -(-1, 'DISOBEY', 'Maybe later...'), -(-1, 'DISOBEY', 'I don''t feel like it'), -(-1, 'DISOBEY', '*pretends not to hear*'), -(-1, 'DISOBEY', 'Nah...'), -(-1, 'DISOBEY', '*turns away*'), -(-1, 'DISOBEY', 'Not right now'), -(-1, 'DISOBEY', '*yawns dismissively*'), -(-1, 'DISOBEY', 'Too tired for that'), -(-1, 'DISOBEY', 'Ask me again later'), -(-1, 'DISOBEY', '*looks the other way*'), -(-1, 'DISOBEY', 'I''d rather not'), -(-1, 'DISOBEY', '*shakes head*'), -(-1, 'DISOBEY', 'No thanks'), -(-1, 'DISOBEY', '*walks away slowly*'), -(-1, 'DISOBEY', 'Can''t be bothered'), -(-1, 'DISOBEY', '*pretends to be asleep*'), -(-1, 'DISOBEY', 'You can''t make me!'), -(-1, 'DISOBEY', '*stubbornly sits down*'), -(-1, 'DISOBEY', 'I refuse!'); - --- DRINKING - When drinking water -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'DRINKING', '*laps up water*'), -(-1, 'DRINKING', 'Refreshing!'), -(-1, 'DRINKING', '*gulp gulp*'), -(-1, 'DRINKING', 'Ah, that''s good!'), -(-1, 'DRINKING', '*slurp*'); - --- EATING - When eating food -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'EATING', '*munches happily*'), -(-1, 'EATING', 'Yum!'), -(-1, 'EATING', 'Delicious!'), -(-1, 'EATING', '*nom nom nom*'), -(-1, 'EATING', 'This is tasty!'), -(-1, 'EATING', '*chomps*'), -(-1, 'EATING', 'More please!'); - --- PLAYFUL - When in playful mood -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'PLAYFUL', '*bounces excitedly*'), -(-1, 'PLAYFUL', 'Let''s play!'), -(-1, 'PLAYFUL', '*runs around happily*'), -(-1, 'PLAYFUL', 'Play with me!'), -(-1, 'PLAYFUL', '*jumps around*'), -(-1, 'PLAYFUL', 'I wanna play!'), -(-1, 'PLAYFUL', '*brings a toy*'), -(-1, 'PLAYFUL', 'Wheee!'), -(-1, 'PLAYFUL', '*zooms around the room*'), -(-1, 'PLAYFUL', 'Catch me if you can!'); - --- SLEEPING - When sleeping -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'SLEEPING', '*snores softly*'), -(-1, 'SLEEPING', 'Zzz...'), -(-1, 'SLEEPING', '*mumbles in sleep*'), -(-1, 'SLEEPING', 'ZzZzZz...'), -(-1, 'SLEEPING', '*dreams peacefully*'), -(-1, 'SLEEPING', '*twitches while dreaming*'), -(-1, 'SLEEPING', '*snoozes*'), -(-1, 'SLEEPING', '*breathes slowly*'); - --- TIRED - When tired/low energy -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'TIRED', '*yawns*'), -(-1, 'TIRED', 'So sleepy...'), -(-1, 'TIRED', '*eyes drooping*'), -(-1, 'TIRED', 'I need rest...'); - --- THIRSTY - When thirsty -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'THIRSTY', '*pants*'), -(-1, 'THIRSTY', 'Water please!'), -(-1, 'THIRSTY', '*looks at water bowl*'), -(-1, 'THIRSTY', 'So thirsty...'), -(-1, 'THIRSTY', '*dry tongue*'), -(-1, 'THIRSTY', 'Need a drink!'), -(-1, 'THIRSTY', '*licks lips*'), -(-1, 'THIRSTY', 'I''m parched!'); - --- HUNGRY - When hungry -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'HUNGRY', '*stomach growls*'), -(-1, 'HUNGRY', 'I need food!'), -(-1, 'HUNGRY', '*looks at food bowl*'), -(-1, 'HUNGRY', 'Feed me!'), -(-1, 'HUNGRY', 'So hungry...'), -(-1, 'HUNGRY', '*tummy rumbles*'), -(-1, 'HUNGRY', 'Food please!'), -(-1, 'HUNGRY', '*drools at thought of food*'), -(-1, 'HUNGRY', 'Is it dinner time?'), -(-1, 'HUNGRY', '*sniffs around for food*'), -(-1, 'HUNGRY', 'I could eat a horse!'), -(-1, 'HUNGRY', '*begs for food*'), -(-1, 'HUNGRY', 'Starving over here!'); - --- GENERIC_NEUTRAL - Random idle chat -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'GENERIC_NEUTRAL', '*looks around*'), -(-1, 'GENERIC_NEUTRAL', '*sniffs the air*'), -(-1, 'GENERIC_NEUTRAL', '*stretches*'), -(-1, 'GENERIC_NEUTRAL', '*scratches ear*'), -(-1, 'GENERIC_NEUTRAL', '*observes surroundings*'), -(-1, 'GENERIC_NEUTRAL', '*sits quietly*'), -(-1, 'GENERIC_NEUTRAL', '*watches curiously*'); - --- GENERIC_SAD - When sad/low happiness -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'GENERIC_SAD', '*whimpers*'), -(-1, 'GENERIC_SAD', '*looks sad*'), -(-1, 'GENERIC_SAD', '*sighs*'), -(-1, 'GENERIC_SAD', '*droops head*'), -(-1, 'GENERIC_SAD', 'I''m lonely...'), -(-1, 'GENERIC_SAD', '*mopes around*'), -(-1, 'GENERIC_SAD', '*looks dejected*'), -(-1, 'GENERIC_SAD', 'Nobody loves me...'), -(-1, 'GENERIC_SAD', '*sulks in corner*'); - --- GENERIC_HAPPY - When happy -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(-1, 'GENERIC_HAPPY', '*wags tail happily*'), -(-1, 'GENERIC_HAPPY', '*jumps with joy*'), -(-1, 'GENERIC_HAPPY', ':)'), -(-1, 'GENERIC_HAPPY', 'Life is good!'), -(-1, 'GENERIC_HAPPY', '*prances around*'), -(-1, 'GENERIC_HAPPY', '*does a happy dance*'), -(-1, 'GENERIC_HAPPY', 'I''m so happy!'), -(-1, 'GENERIC_HAPPY', '*beams with joy*'), -(-1, 'GENERIC_HAPPY', 'What a great day!'), -(-1, 'GENERIC_HAPPY', '*grins*'), -(-1, 'GENERIC_HAPPY', '*radiates happiness*'), -(-1, 'GENERIC_HAPPY', 'Yippee!'), -(-1, 'GENERIC_HAPPY', '*spins around happily*'), -(-1, 'GENERIC_HAPPY', 'This is the best!'); - --- ===================================================== --- PET-SPECIFIC VOCALS --- ===================================================== - --- Dog (0) specific vocals -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(0, 'GENERIC_HAPPY', 'Woof woof!'), -(0, 'GENERIC_HAPPY', '*wags tail furiously*'), -(0, 'GREET_OWNER', '*barks excitedly*'), -(0, 'GREET_OWNER', 'Woof! You''re home!'), -(0, 'PLAYFUL', '*drops ball at your feet*'), -(0, 'PLAYFUL', 'Throw the ball!'), -(0, 'HUNGRY', '*stares at food bowl*'), -(0, 'DISOBEY', '*chases own tail instead*'); - --- Cat (1) specific vocals -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(1, 'GENERIC_HAPPY', '*purrs*'), -(1, 'GENERIC_HAPPY', 'Meow!'), -(1, 'GENERIC_NEUTRAL', '*grooms self*'), -(1, 'GREET_OWNER', '*rubs against leg*'), -(1, 'DISOBEY', '*looks away disdainfully*'), -(1, 'DISOBEY', '*yawns dismissively*'), -(1, 'PLAYFUL', '*pounces on shadow*'), -(1, 'SLEEPING', '*purrs while sleeping*'); - --- Dragon (12) specific vocals -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(12, 'GENERIC_HAPPY', '*breathes small flames happily*'), -(12, 'GENERIC_HAPPY', '*roars softly*'), -(12, 'GENERIC_SAD', '*smoke puffs from nostrils*'), -(12, 'DISOBEY', '*snorts flames*'), -(12, 'DISOBEY', 'I am a DRAGON, not a servant!'), -(12, 'HUNGRY', '*eyes the nearest villager*'), -(12, 'HUNGRY', 'I require sustenance!'), -(12, 'THIRSTY', '*smoke rises as throat dries*'), -(12, 'PLAYFUL', '*chases own tail, breathing fire*'), -(12, 'GREET_OWNER', '*bows majestic head*'), -(12, 'SLEEPING', '*snores, causing small fires*'), -(12, 'LEVEL_UP', '*ROARS triumphantly!*'); - --- Horse (15) specific vocals -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(15, 'GENERIC_HAPPY', '*neighs happily*'), -(15, 'GENERIC_NEUTRAL', '*swishes tail*'), -(15, 'GREET_OWNER', '*whinnies in greeting*'), -(15, 'DISOBEY', 'Nay. (Geddit?)'), -(15, 'HUNGRY', '*looks at hay expectantly*'), -(15, 'PLAYFUL', 'Let''s go for a ride!'), -(15, 'TIRED', '*stamps hoof wearily*'); - --- Tarantula (8) specific vocals -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(8, 'GREET_OWNER', 'You look more edible every time!'), -(8, 'DISOBEY', '*hisses*'), -(8, 'DISOBEY', 'I do not obey mammals'), -(8, 'HUNGRY', 'Bring me fresh meat!'), -(8, 'PLAYFUL', '*dances on eight legs*'), -(8, 'GENERIC_HAPPY', '*clicks mandibles happily*'); - --- Frog (11) specific vocals -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(11, 'GENERIC_HAPPY', 'Ribbit!'), -(11, 'GENERIC_NEUTRAL', '*croaks*'), -(11, 'GREET_OWNER', '*hops excitedly*'), -(11, 'PLAYFUL', '*catches fly with tongue*'), -(11, 'THIRSTY', '*seeks water*'); - --- Cow (35) specific vocals -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(35, 'GENERIC_HAPPY', 'Moooo!'), -(35, 'GREET_OWNER', 'Greetings. Did you bring kale?'), -(35, 'EATING', '*chews grass thoughtfully*'), -(35, 'DISOBEY', 'I''d rather meditate'), -(35, 'LEVEL_UP', '*DING* I''m on the up!'); - --- Lion (6) specific vocals -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(6, 'GENERIC_HAPPY', '*roars majestically*'), -(6, 'GREET_OWNER', '*nods regally*'), -(6, 'DISOBEY', 'I am the king!'), -(6, 'HUNGRY', '*eyes prey*'), -(6, 'PLAYFUL', '*pounces playfully*'); - --- Bear (4) specific vocals -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(4, 'GENERIC_HAPPY', '*growls contentedly*'), -(4, 'HUNGRY', '*sniffs for honey*'), -(4, 'SLEEPING', '*hibernates*'), -(4, 'GREET_OWNER', '*bear hug incoming*'); - --- Monkey (14) specific vocals -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(14, 'GENERIC_HAPPY', 'Ook ook!'), -(14, 'PLAYFUL', '*swings from furniture*'), -(14, 'GREET_OWNER', '*does a backflip*'), -(14, 'DISOBEY', '*throws something*'), -(14, 'HUNGRY', '*looks for bananas*'); - --- Bunny (17) specific vocals -INSERT INTO `pet_vocals` (`pet_id`, `type`, `message`) VALUES -(17, 'GENERIC_HAPPY', '*hops happily*'), -(17, 'GREET_OWNER', '*twitches nose excitedly*'), -(17, 'PLAYFUL', '*binkies around*'), -(17, 'HUNGRY', '*nibbles on carrot*'); - -SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/Database Updates/12012026_Battle Banzai.sql b/Database Updates/12012026_Battle Banzai.sql deleted file mode 100644 index 1e9c9461..00000000 --- a/Database Updates/12012026_Battle Banzai.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET NAMES utf8mb4; -SET FOREIGN_KEY_CHECKS = 0; - --- ===================================================== --- Update 4.0.2-beta to 4.0.3-beta --- ===================================================== - -INSERT INTO `emulator_settings` (`key`, `value`) VALUES --- Maximum pending flood-fill tasks in the executor queue --- Prevents memory leaks from rapid tile locking -('hotel.banzai.fill.max_queue', '50'), - --- Minimum interval (ms) between flood-fill calculations per game --- Prevents errors via rapid wired triggering -('hotel.banzai.fill.cooldown_ms', '100') -ON DUPLICATE KEY UPDATE `value` = VALUES(`value`); - -SET FOREIGN_KEY_CHECKS = 1; diff --git a/Database Updates/12012026_Breeding Fixes.sql b/Database Updates/12012026_Breeding Fixes.sql deleted file mode 100644 index 52825f2b..00000000 --- a/Database Updates/12012026_Breeding Fixes.sql +++ /dev/null @@ -1,216 +0,0 @@ --- ===================================================== --- Pet Breeding Complete Setup --- ===================================================== --- This file sets up all breeding-related data: --- 1. pet_breeding - Maps parent pet types to offspring types --- 2. pet_breeding_races - Defines possible breeds/colors for offspring by rarity --- ===================================================== - --- ===================================================== --- SECTION 1: Pet Breeding (Parent -> Offspring Mapping) --- ===================================================== --- This table maps which pet type produces which baby type - -CREATE TABLE IF NOT EXISTS `pet_breeding` ( - `pet_id` int(11) NOT NULL COMMENT 'Parent pet type', - `offspring_id` int(11) NOT NULL COMMENT 'Baby pet type', - PRIMARY KEY (`pet_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- Clear existing data -TRUNCATE TABLE `pet_breeding`; - --- Insert breeding mappings -INSERT INTO `pet_breeding` (`pet_id`, `offspring_id`) VALUES -(0, 29), -- Dog -> Baby Dog -(1, 28), -- Cat -> Baby Cat -(3, 25), -- Terrier -> Baby Terrier -(4, 24), -- Bear -> Baby Bear -(5, 30); -- Pig -> Baby Pig - --- ===================================================== --- SECTION 2: Pet Breeding Races (Offspring Breeds by Rarity) --- ===================================================== --- rarity_level: 1=Common, 2=Uncommon, 3=Rare, 4=Epic --- breed: The visual breed/color variant of the baby pet --- --- Higher rarity = harder to get, more special colors --- Each baby pet type should have breeds at all 4 rarity levels - -CREATE TABLE IF NOT EXISTS `pet_breeding_races` ( - `pet_id` int(11) NOT NULL COMMENT 'Baby pet type (offspring)', - `rarity_level` int(11) NOT NULL COMMENT '1=Common, 2=Uncommon, 3=Rare, 4=Epic', - `breed` int(11) NOT NULL COMMENT 'Visual breed/color variant', - PRIMARY KEY (`pet_id`, `rarity_level`, `breed`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- Clear existing data -TRUNCATE TABLE `pet_breeding_races`; - --- ===================================================== --- Baby Dog (29) - Offspring of Dog (0) - 20 breeds --- ===================================================== -INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES --- Common breeds (rarity 1) - Most likely to get -(29, 1, 0), -(29, 1, 1), -(29, 1, 2), -(29, 1, 3), -(29, 1, 4), -(29, 1, 5), -(29, 1, 6), -(29, 1, 7), --- Uncommon breeds (rarity 2) -(29, 2, 8), -(29, 2, 9), -(29, 2, 10), -(29, 2, 11), -(29, 2, 12), --- Rare breeds (rarity 3) -(29, 3, 13), -(29, 3, 14), -(29, 3, 15), -(29, 3, 16), --- Epic breeds (rarity 4) - Hardest to get -(29, 4, 17), -(29, 4, 18), -(29, 4, 19); - --- ===================================================== --- Baby Cat (28) - Offspring of Cat (1) - 20 breeds --- ===================================================== -INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES --- Common breeds (rarity 1) -(28, 1, 0), -(28, 1, 1), -(28, 1, 2), -(28, 1, 3), -(28, 1, 4), -(28, 1, 5), -(28, 1, 6), -(28, 1, 7), --- Uncommon breeds (rarity 2) -(28, 2, 8), -(28, 2, 9), -(28, 2, 10), -(28, 2, 11), -(28, 2, 12), --- Rare breeds (rarity 3) -(28, 3, 13), -(28, 3, 14), -(28, 3, 15), -(28, 3, 16), --- Epic breeds (rarity 4) -(28, 4, 17), -(28, 4, 18), -(28, 4, 19); - --- ===================================================== --- Baby Terrier (25) - Offspring of Terrier (3) - 20 breeds --- ===================================================== -INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES --- Common breeds (rarity 1) -(25, 1, 0), -(25, 1, 1), -(25, 1, 2), -(25, 1, 3), -(25, 1, 4), -(25, 1, 5), -(25, 1, 6), -(25, 1, 7), --- Uncommon breeds (rarity 2) -(25, 2, 8), -(25, 2, 9), -(25, 2, 10), -(25, 2, 11), -(25, 2, 12), --- Rare breeds (rarity 3) -(25, 3, 13), -(25, 3, 14), -(25, 3, 15), -(25, 3, 16), --- Epic breeds (rarity 4) -(25, 4, 17), -(25, 4, 18), -(25, 4, 19); - --- ===================================================== --- Baby Bear (24) - Offspring of Bear (4) - 20 breeds --- ===================================================== -INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES --- Common breeds (rarity 1) -(24, 1, 0), -(24, 1, 1), -(24, 1, 2), -(24, 1, 3), -(24, 1, 4), -(24, 1, 5), -(24, 1, 6), -(24, 1, 7), --- Uncommon breeds (rarity 2) -(24, 2, 8), -(24, 2, 9), -(24, 2, 10), -(24, 2, 11), -(24, 2, 12), --- Rare breeds (rarity 3) -(24, 3, 13), -(24, 3, 14), -(24, 3, 15), -(24, 3, 16), --- Epic breeds (rarity 4) -(24, 4, 17), -(24, 4, 18), -(24, 4, 19); - --- ===================================================== --- Baby Pig (30) - Offspring of Pig (5) - 20 breeds --- ===================================================== -INSERT INTO `pet_breeding_races` (`pet_type`, `rarity_level`, `breed`) VALUES --- Common breeds (rarity 1) -(30, 1, 0), -(30, 1, 1), -(30, 1, 2), -(30, 1, 3), -(30, 1, 4), -(30, 1, 5), -(30, 1, 6), -(30, 1, 7), --- Uncommon breeds (rarity 2) -(30, 2, 8), -(30, 2, 9), -(30, 2, 10), -(30, 2, 11), -(30, 2, 12), --- Rare breeds (rarity 3) -(30, 3, 13), -(30, 3, 14), -(30, 3, 15), -(30, 3, 16), --- Epic breeds (rarity 4) -(30, 4, 17), -(30, 4, 18), -(30, 4, 19); - --- ===================================================== --- Also ensure pet_actions has correct offspring_type values --- ===================================================== -UPDATE `pet_actions` SET `offspring_type` = 29 WHERE `pet_type` = 0; -- Dog -> Baby Dog -UPDATE `pet_actions` SET `offspring_type` = 28 WHERE `pet_type` = 1; -- Cat -> Baby Cat -UPDATE `pet_actions` SET `offspring_type` = 25 WHERE `pet_type` = 3; -- Terrier -> Baby Terrier -UPDATE `pet_actions` SET `offspring_type` = 24 WHERE `pet_type` = 4; -- Bear -> Baby Bear -UPDATE `pet_actions` SET `offspring_type` = 30 WHERE `pet_type` = 5; -- Pig -> Baby Pig - --- Set non-breedable pets to -1 -UPDATE `pet_actions` SET `offspring_type` = -1 WHERE `pet_type` NOT IN (0, 1, 3, 4, 5); - --- ===================================================== --- Fix any items_base with leading/trailing spaces in interaction_type --- ===================================================== -UPDATE `items_base` SET `interaction_type` = TRIM(`interaction_type`); - --- ===================================================== --- Ensure breeding nest items have correct interaction_type --- ===================================================== -UPDATE `items_base` SET `interaction_type` = 'breeding_nest' -WHERE `item_name` LIKE 'pet_breeding_%' AND `interaction_type` != 'breeding_nest'; diff --git a/Database Updates/12012026_ChatBubbles.sql b/Database Updates/12012026_ChatBubbles.sql deleted file mode 100644 index e955ad1a..00000000 --- a/Database Updates/12012026_ChatBubbles.sql +++ /dev/null @@ -1,15 +0,0 @@ -ALTER TABLE `permissions` ADD COLUMN `cmd_update_chat_bubbles` ENUM('0','1') NOT NULL DEFAULT '0'; - ---New table for custom chat bubbles -CREATE TABLE chat_bubbles ( - type INT(11) PRIMARY KEY AUTO_INCREMENT COMMENT "Only 46 and higher will work", - name VARCHAR(255) NOT NULL DEFAULT '', - permission VARCHAR(255) NOT NULL DEFAULT '', - overridable BOOLEAN NOT NULL DEFAULT TRUE, - triggers_talking_furniture BOOLEAN NOT NULL DEFAULT FALSE -); - ---New texts for update chat bubbles command -INSERT INTO `emulator_texts` (`key`, `value`) VALUES ('commands.keys.cmd_update_chat_bubbles', 'update_chat_bubbles'); -INSERT INTO `emulator_texts` (`key`, `value`) VALUES ('commands.success.cmd_update_chat_bubbles', 'Successfully updated chat bubbles'); -INSERT INTO `emulator_texts` (`key`, `value`) VALUES ('commands.description.cmd_update_chat_bubbles', ':update_chat_bubbles'); \ No newline at end of file diff --git a/Database Updates/16032026_updateall_command.sql b/Database Updates/16032026_updateall_command.sql deleted file mode 100644 index 8162bc0a..00000000 --- a/Database Updates/16032026_updateall_command.sql +++ /dev/null @@ -1,6 +0,0 @@ -ALTER TABLE `permissions` ADD `cmd_update_all` ENUM('0','1') NOT NULL DEFAULT '0' AFTER `cmd_update_achievements`; - -INSERT INTO `emulator_texts` (`key`, `value`) VALUES - ('commands.keys.cmd_update_all', 'update_all'), - ('commands.description.cmd_update_all', ':update_all'), - ('commands.succes.cmd_update_all', 'Successfully updated everything!'); diff --git a/Database Updates/17032026_allow_underpass.sql b/Database Updates/17032026_allow_underpass.sql deleted file mode 100644 index c5c7dcac..00000000 --- a/Database Updates/17032026_allow_underpass.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `rooms` ADD COLUMN `allow_underpass` ENUM('0','1') NOT NULL DEFAULT '0' AFTER `move_diagonally`; diff --git a/Database Updates/Default_Camera.sql b/Database Updates/Default_Camera.sql deleted file mode 100644 index 63240398..00000000 --- a/Database Updates/Default_Camera.sql +++ /dev/null @@ -1,71 +0,0 @@ --- ============================================================ --- Camera - Database Setup --- Run this SQL manually before using the camera feature. --- ============================================================ - --- ----------------------------------------- --- Table: camera_web (stores published photos) --- ----------------------------------------- -CREATE TABLE IF NOT EXISTS `camera_web` ( - `id` INT(11) NOT NULL AUTO_INCREMENT, - `user_id` INT(11) NOT NULL, - `room_id` INT(11) NOT NULL DEFAULT 0, - `timestamp` INT(11) NOT NULL DEFAULT 0, - `url` VARCHAR(255) NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - INDEX `idx_camera_web_user_id` (`user_id`), - INDEX `idx_camera_web_timestamp` (`timestamp`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- ----------------------------------------- --- Emulator Settings for Camera --- ----------------------------------------- --- Uses INSERT IGNORE so existing values are not overwritten. - --- Base URL where camera photos are served (include trailing slash) -INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES -('camera.url', 'http://localhost/camera/'); - --- Filesystem path where full-size camera photos are saved (include trailing slash) -INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES -('imager.location.output.camera', '/path/to/www/camera/'); - --- Filesystem path where room thumbnail images are saved (include trailing slash) -INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES -('imager.location.output.thumbnail', '/path/to/www/thumbnails/'); - --- Item ID for the wall photo item (must exist in items_base with interaction type "external_image") -INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES -('camera.item_id', '0'); - --- Price in credits to purchase a photo as a wall item -INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES -('camera.price.credits', '2'); - --- Price in seasonal points to purchase a photo as a wall item -INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES -('camera.price.points', '0'); - --- Price in seasonal points to publish a photo to the web -INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES -('camera.price.points.publish', '1'); - --- JSON template for photo item extradata --- Available placeholders: %timestamp%, %room_id%, %url%, %id% -INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES -('camera.extradata', '{"t":"%timestamp%","u":"%id%","m":"","s":"%room_id%","w":"%url%"}'); - --- ----------------------------------------- --- Emulator Texts for Camera --- ----------------------------------------- -INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES -('camera.permission', 'You do not have permission to use the camera.'); - -INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES -('camera.wait', 'Please wait %seconds% more seconds before taking another photo.'); - -INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES -('camera.error.creation', 'An error occurred while processing your photo. Please try again.'); - -INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES -('camera.daily.limit', 'You have reached the daily photo limit. Try again tomorrow.'); diff --git a/Database Updates/Items_Base/update_all_interaction_types_wired.sql b/Database Updates/Items_Base/update_all_interaction_types_wired.sql new file mode 100644 index 00000000..f8053225 --- /dev/null +++ b/Database Updates/Items_Base/update_all_interaction_types_wired.sql @@ -0,0 +1,161 @@ +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_time_more_than' WHERE `public_name` = 'wf_cnd_time_more_than'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_time_less_than' WHERE `public_name` = 'wf_cnd_time_less_than'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_give_reward' WHERE `public_name` = 'wf_act_give_reward'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_call_stacks' WHERE `public_name` = 'wf_act_call_stacks'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_neg_call_stack' WHERE `public_name` = 'wf_act_neg_call_stack'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_neg_call_stacks' WHERE `public_name` = 'wf_act_neg_call_stacks'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_maze'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_give_score_tm' WHERE `public_name` = 'wf_act_give_score_tm'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_move_to_dir' WHERE `public_name` = 'wf_act_move_to_dir'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_leave_team' WHERE `public_name` = 'wf_act_leave_team'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_actor_in_team' WHERE `public_name` = 'wf_cnd_actor_in_team'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_flee' WHERE `public_name` = 'wf_act_flee'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_join_team' WHERE `public_name` = 'wf_act_join_team'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_in_team' WHERE `public_name` = 'wf_cnd_not_in_team'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_furni_on' WHERE `public_name` = 'wf_cnd_not_furni_on'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_stuff_is' WHERE `public_name` = 'wf_cnd_stuff_is'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_stuff_is' WHERE `public_name` = 'wf_cnd_not_stuff_is'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_date_rng_active' WHERE `public_name` = 'wf_cnd_date_rng_active'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_bot_clothes' WHERE `public_name` = 'wf_act_bot_clothes'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_bot_teleport' WHERE `public_name` = 'wf_act_bot_teleport'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_bot_follow_avatar' WHERE `public_name` = 'wf_act_bot_follow_avatar'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_bot_give_handitem' WHERE `public_name` = 'wf_act_bot_give_handitem'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_bot_move' WHERE `public_name` = 'wf_act_bot_move'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_has_handitem' WHERE `public_name` = 'wf_cnd_has_handitem'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_bot_talk_to_avatar' WHERE `public_name` = 'wf_act_bot_talk_to_avatar'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_bot_reached_avtr' WHERE `public_name` = 'wf_trg_bot_reached_avtr'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_bot_talk' WHERE `public_name` = 'wf_act_bot_talk'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_move_rotate' WHERE `public_name` = 'wf_act_move_rotate'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_wire2'; +UPDATE `items_base` SET `interaction_type` = 'switch' WHERE `public_name` = 'wf_floor_switch2'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_state_changed' WHERE `public_name` = 'wf_trg_state_changed'; +UPDATE `items_base` SET `interaction_type` = 'wf_xtra_random' WHERE `public_name` = 'wf_xtra_random'; +UPDATE `items_base` SET `interaction_type` = 'wf_xtra_unseen' WHERE `public_name` = 'wf_xtra_unseen'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_periodically' WHERE `public_name` = 'wf_trg_periodically'; +UPDATE `items_base` SET `interaction_type` = 'pyramid' WHERE `public_name` = 'wf_pyramid'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_score_achieved' WHERE `public_name` = 'wf_trg_score_achieved'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_teleport_to' WHERE `public_name` = 'wf_act_teleport_to'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_says_something' WHERE `public_name` = 'wf_trg_says_something'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_wire4'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_walks_off_furni' WHERE `public_name` = 'wf_trg_walks_off_furni'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_at_given_time' WHERE `public_name` = 'wf_trg_at_given_time'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_game_ends' WHERE `public_name` = 'wf_trg_game_ends'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_show_message' WHERE `public_name` = 'wf_act_show_message'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_collision' WHERE `public_name` = 'wf_trg_collision'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_enter_room' WHERE `public_name` = 'wf_trg_enter_room'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_toggle_state' WHERE `public_name` = 'wf_act_toggle_state'; +UPDATE `items_base` SET `interaction_type` = 'gate' WHERE `public_name` = 'wf_firegate'; +UPDATE `items_base` SET `interaction_type` = 'pressureplate' WHERE `public_name` = 'wf_ringplate'; +UPDATE `items_base` SET `interaction_type` = 'pressureplate' WHERE `public_name` = 'wf_pressureplate'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_glowball'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_reset_timers' WHERE `public_name` = 'wf_act_reset_timers'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_furnis_hv_avtrs' WHERE `public_name` = 'wf_cnd_furnis_hv_avtrs'; +UPDATE `items_base` SET `interaction_type` = 'pressureplate' WHERE `public_name` = 'wf_arrowplate'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_trggrer_on_frn' WHERE `public_name` = 'wf_cnd_trggrer_on_frn'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_wire1'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_give_score' WHERE `public_name` = 'wf_act_give_score'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_wire3'; +UPDATE `items_base` SET `interaction_type` = 'gate' WHERE `public_name` = 'wf_glassdoor'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_match_to_sshot' WHERE `public_name` = 'wf_act_match_to_sshot'; +UPDATE `items_base` SET `interaction_type` = 'switch' WHERE `public_name` = 'wf_floor_switch1'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_game_starts' WHERE `public_name` = 'wf_trg_game_starts'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_walks_on_furni' WHERE `public_name` = 'wf_trg_walks_on_furni'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_actor_in_group' WHERE `public_name` = 'wf_cnd_actor_in_group'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_in_group' WHERE `public_name` = 'wf_cnd_not_in_group'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_trggrer_on' WHERE `public_name` = 'wf_cnd_not_trggrer_on'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_hv_avtrs' WHERE `public_name` = 'wf_cnd_not_hv_avtrs'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_user_count_in' WHERE `public_name` = 'wf_cnd_user_count_in'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_user_count' WHERE `public_name` = 'wf_cnd_not_user_count'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_wearing_effect' WHERE `public_name` = 'wf_cnd_wearing_effect'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_wearing_fx' WHERE `public_name` = 'wf_cnd_not_wearing_fx'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_wearing_badge' WHERE `public_name` = 'wf_cnd_wearing_badge'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_wearing_b' WHERE `public_name` = 'wf_cnd_not_wearing_b'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_kick_user' WHERE `public_name` = 'wf_act_kick_user'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_mute_triggerer' WHERE `public_name` = 'wf_act_mute_triggerer'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_match_snapshot' WHERE `public_name` = 'wf_cnd_match_snapshot'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_match_snap' WHERE `public_name` = 'wf_cnd_not_match_snap'; +UPDATE `items_base` SET `interaction_type` = 'wf_blob' WHERE `public_name` = 'wf_blob'; +UPDATE `items_base` SET `interaction_type` = 'wf_blob' WHERE `public_name` = 'wf_blob2'; +UPDATE `items_base` SET `interaction_type` = 'puzzle_box' WHERE `public_name` = 'wf_box'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_has_furni_on' WHERE `public_name` = 'wf_cnd_has_furni_on'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_super_wired' WHERE `public_name` = 'wf_act_super_wired'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_super_wired' WHERE `public_name` = 'wf_cnd_super_wired'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_period_long' WHERE `public_name` = 'wf_trg_period_long'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_bot_reached_stf' WHERE `public_name` = 'wf_trg_bot_reached_stf'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_chase' WHERE `public_name` = 'wf_act_chase'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_move_furni_to' WHERE `public_name` = 'wf_act_move_furni_to'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_toggle_to_rnd' WHERE `public_name` = 'wf_act_toggle_to_rnd'; +UPDATE `items_base` SET `interaction_type` = 'wf_blob' WHERE `public_name` = 'wf_blob2_vis'; +UPDATE `items_base` SET `interaction_type` = 'wf_blob' WHERE `public_name` = 'wf_blob_invis'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_at_time_long' WHERE `public_name` = 'wf_trg_at_time_long'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_control_clock' WHERE `public_name` = 'wf_act_control_clock'; +UPDATE `items_base` SET `interaction_type` = 'game_upcounter' WHERE `public_name` = 'wf_game_upcounter1'; +UPDATE `items_base` SET `interaction_type` = 'game_upcounter' WHERE `public_name` = 'wf_game_upcounter2'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_clock_counter' WHERE `public_name` = 'wf_trg_clock_counter'; +UPDATE `items_base` SET `interaction_type` = 'wf_xtra_or_eval' WHERE `public_name` = 'wf_xtra_or_eval'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_test_act'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_test_cnd'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_test_trg'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_test_xtra'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_counter_time_matches' WHERE `public_name` = 'wf_cnd_counter_time_matches'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_match_date' WHERE `public_name` = 'wf_cnd_match_date'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_match_time' WHERE `public_name` = 'wf_cnd_match_time'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_has_handitem' WHERE `public_name` = 'wf_cnd_not_has_handitem'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_triggerer_match' WHERE `public_name` = 'wf_cnd_not_triggerer_match'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_user_performs_action' WHERE `public_name` = 'wf_cnd_not_user_performs_action'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_team_has_rank' WHERE `public_name` = 'wf_cnd_team_has_rank'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_team_has_score' WHERE `public_name` = 'wf_cnd_team_has_score'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_triggerer_match' WHERE `public_name` = 'wf_cnd_triggerer_match'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_user_performs_action' WHERE `public_name` = 'wf_cnd_user_performs_action'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_user_performs_action' WHERE `public_name` = 'wf_trg_user_performs_action'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_freeze' WHERE `public_name` = 'wf_act_freeze'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_furni_to_furni' WHERE `public_name` = 'wf_act_furni_to_furni'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_furni_to_user' WHERE `public_name` = 'wf_act_furni_to_user'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_rel_mov' WHERE `public_name` = 'wf_act_rel_mov'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_send_signal' WHERE `public_name` = 'wf_act_send_signal'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_neg_send_signal' WHERE `public_name` = 'wf_act_neg_send_signal'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_set_altitude' WHERE `public_name` = 'wf_act_set_altitude'; +UPDATE `items_base` SET `interaction_type` = 'wf_act_unfreeze' WHERE `public_name` = 'wf_act_unfreeze'; +UPDATE `items_base` SET `interaction_type` = 'antenna' WHERE `public_name` = 'wf_antenna1'; +UPDATE `items_base` SET `interaction_type` = 'antenna' WHERE `public_name` = 'wf_antenna2'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_actor_dir' WHERE `public_name` = 'wf_cnd_actor_dir'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_has_altitude' WHERE `public_name` = 'wf_cnd_has_altitude'; +UPDATE `items_base` SET `interaction_type` = 'wf_cnd_slc_quantity' WHERE `public_name` = 'wf_cnd_slc_quantity'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_numbertile1'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_numbertile2'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_furni_altitude' WHERE `public_name` = 'wf_slc_furni_altitude'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_furni_area' WHERE `public_name` = 'wf_slc_furni_area'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_furni_bytype' WHERE `public_name` = 'wf_slc_furni_bytype'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_furni_neighborhood' WHERE `public_name` = 'wf_slc_furni_neighborhood'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_furni_onfurni' WHERE `public_name` = 'wf_slc_furni_onfurni'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_furni_picks' WHERE `public_name` = 'wf_slc_furni_picks'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_furni_signal' WHERE `public_name` = 'wf_slc_furni_signal'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_area' WHERE `public_name` = 'wf_slc_users_area'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_byaction' WHERE `public_name` = 'wf_slc_users_byaction'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_byname' WHERE `public_name` = 'wf_slc_users_byname'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_bytype' WHERE `public_name` = 'wf_slc_users_bytype'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_group' WHERE `public_name` = 'wf_slc_users_group'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_handitem' WHERE `public_name` = 'wf_slc_users_handitem'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_neighborhood' WHERE `public_name` = 'wf_slc_users_neighborhood'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_onfurni' WHERE `public_name` = 'wf_slc_users_onfurni'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_signal' WHERE `public_name` = 'wf_slc_users_signal'; +UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_team' WHERE `public_name` = 'wf_slc_users_team'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_test_slc'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_tile1'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_tile2'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_click_furni' WHERE `public_name` = 'wf_trg_click_furni'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_period_short' WHERE `public_name` = 'wf_trg_period_short'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_recv_signal' WHERE `public_name` = 'wf_trg_recv_signal'; +UPDATE `items_base` SET `interaction_type` = 'wf_trg_stuff_state' WHERE `public_name` = 'wf_trg_stuff_state'; +UPDATE `items_base` SET `interaction_type` = 'wf_xtra_anim_time' WHERE `public_name` = 'wf_xtra_anim_time'; +UPDATE `items_base` SET `interaction_type` = 'wf_xtra_execution_limit' WHERE `public_name` = 'wf_xtra_execution_limit'; +UPDATE `items_base` SET `interaction_type` = 'wf_xtra_filter_furni' WHERE `public_name` = 'wf_xtra_filter_furni'; +UPDATE `items_base` SET `interaction_type` = 'wf_xtra_filter_users' WHERE `public_name` = 'wf_xtra_filter_users'; +UPDATE `items_base` SET `interaction_type` = 'wf_xtra_mov_carry_users' WHERE `public_name` = 'wf_xtra_mov_carry_users'; +UPDATE `items_base` SET `interaction_type` = 'wf_xtra_mov_no_animation' WHERE `public_name` = 'wf_xtra_mov_no_animation'; +UPDATE `items_base` SET `interaction_type` = 'wf_xtra_mov_physics' WHERE `public_name` = 'wf_xtra_mov_physics'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_act_log'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_act_neg_log'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_var_echo'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_xtra_var_lvlup_system'; +UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_xtra_var_time_util'; diff --git a/Database Updates/Items_Base/update_wired.sql b/Database Updates/Items_Base/update_wired.sql new file mode 100644 index 00000000..66f4e140 --- /dev/null +++ b/Database Updates/Items_Base/update_wired.sql @@ -0,0 +1,11 @@ +UPDATE items_base SET customparams = 'is_invisible' WHERE public_name like 'tile_stackmagic%'; +UPDATE items_base SET customparams = 'is_invisible' WHERE public_name like 'tile_walkmagic%'; +UPDATE items_base SET customparams = 'is_invisible' WHERE public_name like 'room_invisible_block%'; +UPDATE items_base SET customparams = 'is_invisible' WHERE public_name = 'room_invisible_sit_tile'; +UPDATE items_base SET customparams = 'is_invisible' WHERE public_name = 'room_invisible_click_tile'; + +UPDATE `items_base` SET `interaction_type` = 'wf_conf_invis_control' WHERE `public_name` = 'conf_invis_control'; +UPDATE `items_base` SET `interaction_type` = 'wf_conf_handitem_block' WHERE `public_name` = 'conf_handitem_block'; +UPDATE `items_base` SET `interaction_type` = 'wf_conf_wired_disable' WHERE `public_name` = 'conf_wired_disable'; +UPDATE `items_base` SET `interaction_type` = 'wf_conf_queue_speed' WHERE `public_name` = 'conf_queue_speed'; +UPDATE `items_base` SET `interaction_type` = 'wf_conf_area_hide' WHERE `public_name` = 'conf_area_hide'; \ No newline at end of file diff --git a/Database Updates/UpdateDatabase_Allow_diagonale.sql b/Database Updates/UpdateDatabase_Allow_diagonale.sql deleted file mode 100644 index 7683c489..00000000 --- a/Database Updates/UpdateDatabase_Allow_diagonale.sql +++ /dev/null @@ -1 +0,0 @@ -INSERT INTO emulator_settings (`key`, `value`) VALUES ('pathfinder.diagonal.enabled', '1'); diff --git a/Database Updates/UpdateDatabase_BOT.sql b/Database Updates/UpdateDatabase_BOT.sql deleted file mode 100644 index 67382dde..00000000 --- a/Database Updates/UpdateDatabase_BOT.sql +++ /dev/null @@ -1,4 +0,0 @@ -### New bot walking settings - -INSERT INTO `emulator_settings` (`key`, `value`) VALUES ('hotel.bot.limit.walking.distance', '1'); -INSERT INTO `emulator_settings` (`key`, `value`) VALUES ('hotel.bot.limit.walking.distance.radius', '5'); \ No newline at end of file diff --git a/Database Updates/UpdateDatabase_Banners.sql b/Database Updates/UpdateDatabase_Banners.sql deleted file mode 100644 index 0792f3d6..00000000 --- a/Database Updates/UpdateDatabase_Banners.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE `users` -ADD COLUMN `background_id` INT(11) NOT NULL DEFAULT 0 AFTER `machine_id`, -ADD COLUMN `background_stand_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_id`, -ADD COLUMN `background_overlay_id` INT(11) NOT NULL DEFAULT 0 AFTER `background_stand_id`; diff --git a/Database Updates/UpdateDatabase_DanceCMD.sql b/Database Updates/UpdateDatabase_DanceCMD.sql deleted file mode 100644 index 8ed80044..00000000 --- a/Database Updates/UpdateDatabase_DanceCMD.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE `camwijs`.`permissions` -ADD COLUMN `cms_dance` ENUM('0', '1') NULL DEFAULT '0' AFTER `cmd_credits`; - -INSERT INTO emulator_texts (`key`, `value`) VALUES ('commands.description.cmd_dance', 'dance around the world ! use 1 t/m 4 and 0 to stop'); -INSERT INTO emulator_texts (`key`, `value`) VALUES ('commands.keys.cmd_dance', 'dance'); diff --git a/Database Updates/UpdateDatabase_Happiness.sql b/Database Updates/UpdateDatabase_Happiness.sql deleted file mode 100644 index 68c3246e..00000000 --- a/Database Updates/UpdateDatabase_Happiness.sql +++ /dev/null @@ -1,4 +0,0 @@ -UPDATE `emulator_texts` SET `key` = 'generic.pet.happiness', `value` = 'Happiness' WHERE `key` = 'generic.pet.happyness'; - -ALTER TABLE `pet_commands_data` CHANGE `cost_happyness` `cost_happiness` int(11) NOT NULL DEFAULT '0'; -ALTER TABLE `users_pets` CHANGE `happyness` `happiness` int(11) NOT NULL DEFAULT '100'; diff --git a/Database Updates/UpdateDatabase_Websocket.sql b/Database Updates/UpdateDatabase_Websocket.sql deleted file mode 100644 index e629c94f..00000000 --- a/Database Updates/UpdateDatabase_Websocket.sql +++ /dev/null @@ -1,6 +0,0 @@ -### WEBSOCKET SETTINGS - -INSERT INTO `emulator_settings` (`key`, `value`) VALUES ('websockets.whitelist', 'localhost'); # Change this to the url of the websocket Expl. ws.mydomain.com -INSERT INTO `emulator_settings` (`key`, `value`) VALUES ('ws.nitro.host', '0.0.0.0'); # Best is this to leave it at 0.0.0.0 -INSERT INTO `emulator_settings` (`key`, `value`) VALUES ('ws.nitro.ip.header', ''); # When useing a proxy change the header : X-Forwarded-For when using a proxy server or CF-Connecting-IP if behind Cloudflare. -INSERT INTO `emulator_settings` (`key`, `value`) VALUES ('ws.nitro.port', '2096'); # set the port of the websocket, cloudflare ports : 443 / 2053 / 2083 / 2087 / 2096 / 8443 \ No newline at end of file diff --git a/Database Updates/UpdateDatabase_unignorable.sql b/Database Updates/UpdateDatabase_unignorable.sql deleted file mode 100644 index bcca3ce4..00000000 --- a/Database Updates/UpdateDatabase_unignorable.sql +++ /dev/null @@ -1,2 +0,0 @@ ---New permission -ALTER TABLE `permissions` ADD COLUMN `acc_unignorable` ENUM('0','1') NOT NULL DEFAULT '0'; diff --git a/Default Database/FullDB.sql b/Default Database/FullDB.sql index 80fc2a10..81660209 100644 --- a/Default Database/FullDB.sql +++ b/Default Database/FullDB.sql @@ -1530,7 +1530,7 @@ CREATE TABLE `catalog_club_offers` ( `credits` int(0) NOT NULL DEFAULT 10, `points` int(0) NOT NULL DEFAULT 0, `points_type` int(0) NOT NULL DEFAULT 0, - `type` enum('HC','VIP') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'HC', + `type` enum('HC','VIP','BUILDERS_CLUB','BUILDERS_CLUB_ADDON') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'HC', `deal` enum('0','1') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT '0', `giftable` enum('1','0') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT '0', PRIMARY KEY (`id`) USING BTREE @@ -13499,7 +13499,7 @@ CREATE TABLE `catalog_pages_bc` ( `id` int(0) NOT NULL AUTO_INCREMENT, `parent_id` int(0) NOT NULL DEFAULT -1, `caption` varchar(128) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL, - `page_layout` enum('default_3x3','club_buy','club_gift','frontpage','spaces','recycler','recycler_info','recycler_prizes','trophies','plasto','marketplace','marketplace_own_items','spaces_new','soundmachine','guilds','guild_furni','info_duckets','info_rentables','info_pets','roomads','single_bundle','sold_ltd_items','badge_display','bots','pets','pets2','pets3','productpage1','room_bundle','recent_purchases','default_3x3_color_grouping','guild_forum','vip_buy','info_loyalty','loyalty_vip_buy','collectibles','petcustomization','frontpage_featured') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'default_3x3', + `page_layout` enum('default_3x3','club_buy','club_gift','frontpage','spaces','recycler','recycler_info','recycler_prizes','trophies','plasto','marketplace','marketplace_own_items','spaces_new','soundmachine','guilds','guild_furni','info_duckets','info_rentables','info_pets','roomads','single_bundle','sold_ltd_items','badge_display','bots','pets','pets2','pets3','productpage1','room_bundle','recent_purchases','default_3x3_color_grouping','guild_forum','vip_buy','info_loyalty','loyalty_vip_buy','collectibles','petcustomization','frontpage_featured','builders_club_frontpage','builders_club_addons','builders_club_loyalty') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'default_3x3', `icon_color` int(0) NOT NULL DEFAULT 1, `icon_image` int(0) NOT NULL DEFAULT 1, `order_num` int(0) NOT NULL DEFAULT 1, @@ -30209,6 +30209,7 @@ CREATE TABLE `users_settings` ( `ui_flags` int(0) NOT NULL DEFAULT 1, `has_gotten_default_saved_searches` tinyint(1) NOT NULL DEFAULT 0, `hc_gifts_claimed` int(0) NULL DEFAULT 0, + `builders_club_bonus_furni` int(0) NOT NULL DEFAULT 0, `last_hc_payday` int(0) NULL DEFAULT 0, `max_rooms` int(0) NULL DEFAULT 50, `max_friends` int(0) NULL DEFAULT 300, @@ -30223,7 +30224,7 @@ CREATE TABLE `users_settings` ( -- ---------------------------- -- Records of users_settings -- ---------------------------- -INSERT INTO `users_settings` VALUES (1, 1, 0, 0, 3, 3, 0, 0, 0, '0', '1', '0', 0, 0, 0, 0, 0, 0, 0, '0', '0', '0', 100, 100, 100, '0', '0', 0, 0, 0, 'Arcturus Emulator;', 0, 0, 0, 0, 0, '0', -1, -1, '0', '0', '0', 0, '0', '0', 0, 1, 1, 0, 0, 50, 300); +INSERT INTO `users_settings` VALUES (1, 1, 0, 0, 3, 3, 0, 0, 0, '0', '1', '0', 0, 0, 0, 0, 0, 0, 0, '0', '0', '0', 100, 100, 100, '0', '0', 0, 0, 0, 'Arcturus Emulator;', 0, 0, 0, 0, 0, '0', -1, -1, '0', '0', '0', 0, '0', '0', 0, 1, 1, 0, 0, 0, 50, 300); -- ---------------------------- -- Table structure for users_subscriptions @@ -30449,3 +30450,50 @@ INSERT INTO `youtube_playlists` VALUES (6587, 'PL4YfV2mXS8WXOkxFly7YsGL8cKtqp873 INSERT INTO `youtube_playlists` VALUES (6587, 'PL80F08DAE1B614BA9', 0); SET FOREIGN_KEY_CHECKS = 1; +ALTER TABLE `catalog_pages` + MODIFY COLUMN `page_layout` ENUM( + 'default_3x3', + 'club_buy', + 'club_gift', + 'frontpage', + 'spaces', + 'recycler', + 'recycler_info', + 'recycler_prizes', + 'trophies', + 'plasto', + 'marketplace', + 'marketplace_own_items', + 'spaces_new', + 'soundmachine', + 'guilds', + 'guild_furni', + 'info_duckets', + 'info_rentables', + 'info_pets', + 'roomads', + 'single_bundle', + 'sold_ltd_items', + 'badge_display', + 'bots', + 'pets', + 'pets2', + 'pets3', + 'productpage1', + 'room_bundle', + 'recent_purchases', + 'default_3x3_color_grouping', + 'guild_forum', + 'vip_buy', + 'info_loyalty', + 'loyalty_vip_buy', + 'collectibles', + 'petcustomization', + 'frontpage_featured', + 'builders_club_frontpage', + 'builders_club_addons', + 'builders_club_loyalty' + ) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'default_3x3'; + +ALTER TABLE `catalog_pages` + ADD COLUMN `catalog_mode` ENUM('NORMAL','BUILDER','BOTH') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'NORMAL' AFTER `club_only`; diff --git a/Emulator/.idea/.gitignore b/Emulator/.idea/.gitignore deleted file mode 100644 index 26d33521..00000000 --- a/Emulator/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/Emulator/.idea/compiler.xml b/Emulator/.idea/compiler.xml deleted file mode 100644 index 3376806e..00000000 --- a/Emulator/.idea/compiler.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/Emulator/.idea/encodings.xml b/Emulator/.idea/encodings.xml deleted file mode 100644 index aa00ffab..00000000 --- a/Emulator/.idea/encodings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Emulator/.idea/jarRepositories.xml b/Emulator/.idea/jarRepositories.xml deleted file mode 100644 index 27bf0394..00000000 --- a/Emulator/.idea/jarRepositories.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/Emulator/.idea/misc.xml b/Emulator/.idea/misc.xml deleted file mode 100644 index 5ddb3b31..00000000 --- a/Emulator/.idea/misc.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Emulator/plugins/NitroWebsockets-3.1.jar b/Emulator/plugins/NitroWebsockets-3.1.jar deleted file mode 100644 index 32f73ce5..00000000 Binary files a/Emulator/plugins/NitroWebsockets-3.1.jar and /dev/null differ diff --git a/Emulator/pom.xml b/Emulator/pom.xml index 019470dc..3f2ead4c 100644 --- a/Emulator/pom.xml +++ b/Emulator/pom.xml @@ -6,12 +6,12 @@ com.eu.habbo Habbo - 4.0.5 + 4.1.2 UTF-8 - 21 - 21 + 25 + 25 @@ -19,7 +19,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.13.0 + 3.15.0 19 19 @@ -86,11 +86,11 @@ 2.11.0 - + - com.mysql - mysql-connector-j - 9.1.0 + org.mariadb.jdbc + mariadb-java-client + 3.5.1 runtime diff --git a/Emulator/sqlupdates/catalog_admin_permission.sql b/Emulator/sqlupdates/catalog_admin_permission.sql new file mode 100644 index 00000000..65eb459f --- /dev/null +++ b/Emulator/sqlupdates/catalog_admin_permission.sql @@ -0,0 +1,17 @@ +-- ============================================================ +-- 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/sqlupdates/custom_prefixes_setup.sql b/Emulator/sqlupdates/custom_prefixes_setup.sql new file mode 100644 index 00000000..7d5b22c5 --- /dev/null +++ b/Emulator/sqlupdates/custom_prefixes_setup.sql @@ -0,0 +1,115 @@ +-- ============================================================ +-- Custom Prefix System - Complete Setup +-- ============================================================ + +-- 1. Main user prefixes table +CREATE TABLE IF NOT EXISTS `user_prefixes` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `user_id` INT(11) NOT NULL, + `text` VARCHAR(50) NOT NULL, + `color` VARCHAR(255) NOT NULL DEFAULT '#FFFFFF', + `icon` VARCHAR(50) NOT NULL DEFAULT '', + `effect` VARCHAR(50) NOT NULL DEFAULT '', + `active` TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + INDEX `idx_user_id` (`user_id`), + INDEX `idx_user_active` (`user_id`, `active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Prefix settings table +CREATE TABLE IF NOT EXISTS `custom_prefix_settings` ( + `key_name` VARCHAR(100) NOT NULL, + `value` VARCHAR(255) NOT NULL, + PRIMARY KEY (`key_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Default settings +INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES + ('max_length', '15'), + ('min_rank_to_buy', '1'), + ('price_credits', '5'), + ('price_points', '0'), + ('points_type', '0'); + +-- 3. Blacklisted words table +CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `word` VARCHAR(100) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_word` (`word`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Example blacklist entries (customize as needed) +INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES + ('admin'), + ('staff'), + ('mod'), + ('owner'); + +-- 4. Add effect column (if table already exists without it) +-- ALTER TABLE `user_prefixes` ADD COLUMN IF NOT EXISTS `effect` VARCHAR(50) NOT NULL DEFAULT '' AFTER `icon`; + +-- ============================================================ +-- Catalog page for custom prefixes +-- ============================================================ +-- NOTE: Adjust parent_id to match your catalog parent category ID. +-- Example: parent_id = -1 for root, or the ID of your "Extra" / "Specials" category + +INSERT INTO `catalog_pages` ( + `parent_id`, `caption`, `caption_save`, `icon_image`, `visible`, `enabled`, + `min_rank`, `page_layout`, `page_strings_1`, `page_strings_2` +) VALUES ( + -1, + 'Custom Prefix', + 'custom_prefix', + 1, + 1, + 1, + 1, + 'custom_prefix', + 'Create your own custom prefix!\rChoose text, colors, icon and effects to stand out in chat.', + '' +); + +-- ============================================================ +-- Command texts (insert into emulator_texts if not present) +-- ============================================================ +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES + -- GivePrefix command + ('commands.keys.cmd_give_prefix', 'giveprefix'), + ('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.'); + +-- ============================================================ +-- Permissions for prefix commands (add to permissions table) +-- ============================================================ +INSERT IGNORE INTO `permissions` (`id`, `rank_id`, `permission_name`, `setting_type`) VALUES + (NULL, 7, 'cmd_give_prefix', '1'), + (NULL, 7, 'cmd_list_prefixes', '1'), + (NULL, 7, 'cmd_remove_prefix', '1'), + (NULL, 7, 'cmd_prefix_blacklist', '1'); diff --git a/Emulator/src/main/java/com/eu/habbo/Emulator.java b/Emulator/src/main/java/com/eu/habbo/Emulator.java index 69f1deab..e1b5ab0a 100644 --- a/Emulator/src/main/java/com/eu/habbo/Emulator.java +++ b/Emulator/src/main/java/com/eu/habbo/Emulator.java @@ -7,6 +7,7 @@ import com.eu.habbo.core.*; import com.eu.habbo.core.consolecommands.ConsoleCommand; import com.eu.habbo.database.Database; import com.eu.habbo.habbohotel.GameEnvironment; +import com.eu.habbo.habbohotel.gameclients.SessionResumeManager; import com.eu.habbo.networking.gameserver.GameServer; import com.eu.habbo.networking.rconserver.RCONServer; import com.eu.habbo.plugin.PluginManager; @@ -20,6 +21,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; +import java.net.JarURLConnection; +import java.net.URL; import java.security.MessageDigest; import java.security.SecureRandom; import java.sql.Timestamp; @@ -36,8 +39,8 @@ public final class Emulator { private static final String CLASS_PATH = (System.getProperty("java.class.path") != null ? System.getProperty("java.class.path") : "Unknown"); public final static int MAJOR = 4; - public final static int MINOR = 0; - public final static int BUILD = 5; + public final static int MINOR = 1; + public final static int BUILD = 0; public final static String PREVIEW = ""; public static final String version = "Arcturus Morningstar" + " " + MAJOR + "." + MINOR + "." + BUILD + " " + PREVIEW; @@ -52,6 +55,9 @@ public final class Emulator { "Still Rocking in 2026.\n"; public static String build = ""; + public static long buildTimestamp = -1L; + + public static boolean isReady = false; public static boolean isShuttingDown = false; public static boolean stopped = false; @@ -103,13 +109,6 @@ public final class Emulator { System.out.println(logo); - System.out.println(); - LOGGER.info("https://github.com/duckietm/Arcturus-Morningstar-Extended, "); - System.out.println(); - LOGGER.info("This project is for educational purposes only. This Emulator is an open-source fork of Arcturus created by TheGeneral."); - LOGGER.info("Version: {}", version); - LOGGER.info("Build: {}", build); - long startTime = System.nanoTime(); Emulator.runtime = Runtime.getRuntime(); @@ -141,6 +140,15 @@ public final class Emulator { Emulator.config.register("camera.price.points", "0"); Emulator.config.register("camera.price.points.type", "5"); Emulator.config.register("camera.render.delay", "5"); + Emulator.config.register("hotel.timezone", java.time.ZoneId.systemDefault().getId()); + String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId()); + System.out.println(); + LOGGER.info("https://github.com/duckietm/Arcturus-Morningstar-Extended, "); + System.out.println(); + LOGGER.info("This project is for educational purposes only. This Emulator is an open-source fork of Arcturus created by TheGeneral."); + LOGGER.info("Version: {}", version); + LOGGER.info("Build: {}", build); + LOGGER.info("Build Timestamp: {} [{}]", formatBuildTimestamp(buildTimestamp, hotelTimezoneId), hotelTimezoneId); Emulator.texts.register("camera.permission", "You don't have permission to use the camera!"); Emulator.texts.register("camera.wait", "Please wait %seconds% seconds before making another picture."); Emulator.texts.register("camera.error.creation", "Failed to create your picture. *sadpanda*"); @@ -216,12 +224,21 @@ public final class Emulator { private static void setBuild() { if (Emulator.class.getProtectionDomain().getCodeSource() == null) { build = "UNKNOWN"; + buildTimestamp = -1L; return; } StringBuilder sb = new StringBuilder(); try { - String filepath = new File(Emulator.class.getProtectionDomain().getCodeSource().getLocation().getPath()).getAbsolutePath(); + File buildFile = new File(Emulator.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + buildTimestamp = resolveBuildTimestamp(buildFile); + + if (!buildFile.isFile()) { + build = "DEV"; + return; + } + + String filepath = buildFile.getAbsolutePath(); MessageDigest md = MessageDigest.getInstance("MD5"); try (FileInputStream fis = new FileInputStream(filepath)) { byte[] dataBytes = new byte[1024]; @@ -234,14 +251,69 @@ public final class Emulator { } } catch (Exception e) { build = "UNKNOWN"; + buildTimestamp = -1L; return; } build = sb.toString(); } + private static long resolveBuildTimestamp(File buildFile) { + if (buildFile != null && buildFile.exists() && buildFile.isFile()) { + return buildFile.lastModified(); + } + + try { + URL classUrl = Emulator.class.getResource("Emulator.class"); + + if (classUrl != null) { + if ("file".equalsIgnoreCase(classUrl.getProtocol())) { + File classFile = new File(classUrl.toURI()); + + if (classFile.exists()) { + return classFile.lastModified(); + } + } + + if ("jar".equalsIgnoreCase(classUrl.getProtocol())) { + JarURLConnection connection = (JarURLConnection) classUrl.openConnection(); + File jarFile = new File(connection.getJarFileURL().toURI()); + + if (jarFile.exists()) { + return jarFile.lastModified(); + } + } + } + } catch (Exception ignored) { + } + + if (buildFile != null && buildFile.exists()) { + return buildFile.lastModified(); + } + + return -1L; + } + + private static String formatBuildTimestamp(long buildTimestamp, String timezoneId) { + if (buildTimestamp <= 0) { + return "UNKNOWN"; + } + + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + try { + format.setTimeZone(TimeZone.getTimeZone(java.time.ZoneId.of(timezoneId))); + } catch (Exception ignored) { + format.setTimeZone(TimeZone.getDefault()); + } + + return format.format(new Timestamp(buildTimestamp)); + } + private static void dispose() { - Emulator.getThreading().setCanAdd(false); + if (Emulator.threading != null) { + Emulator.threading.setCanAdd(false); + } Emulator.isShuttingDown = true; Emulator.isReady = false; @@ -250,6 +322,7 @@ public final class Emulator { if (Emulator.pluginManager != null) tryShutdown(() -> Emulator.pluginManager.fireEvent(new EmulatorStartShutdownEvent())); if (Emulator.rconServer != null) tryShutdown(() -> Emulator.rconServer.stop()); + tryShutdown(() -> SessionResumeManager.getInstance().disposeAll()); if (Emulator.gameEnvironment != null) tryShutdown(() -> Emulator.gameEnvironment.dispose()); if (Emulator.pluginManager != null) tryShutdown(() -> Emulator.pluginManager.fireEvent(new EmulatorStoppedEvent())); diff --git a/Emulator/src/main/java/com/eu/habbo/core/CleanerThread.java b/Emulator/src/main/java/com/eu/habbo/core/CleanerThread.java index e9e2f122..ed49246c 100644 --- a/Emulator/src/main/java/com/eu/habbo/core/CleanerThread.java +++ b/Emulator/src/main/java/com/eu/habbo/core/CleanerThread.java @@ -4,7 +4,6 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.guilds.forums.ForumThread; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.incoming.friends.SearchUserEvent; -import com.eu.habbo.messages.incoming.navigator.SearchRoomsEvent; import com.eu.habbo.messages.outgoing.users.UserDataComposer; import com.eu.habbo.threading.runnables.AchievementUpdater; import org.slf4j.Logger; @@ -101,8 +100,7 @@ public class CleanerThread implements Runnable { LAST_HABBO_CACHE_CLEARED = time; } - SearchRoomsEvent.cachedResults.clear(); - SearchUserEvent.cachedResults.clear(); + SearchUserEvent.cleanExpiredCache(); } diff --git a/Emulator/src/main/java/com/eu/habbo/core/ConfigurationManager.java b/Emulator/src/main/java/com/eu/habbo/core/ConfigurationManager.java index 3a6ba211..1b0065b2 100644 --- a/Emulator/src/main/java/com/eu/habbo/core/ConfigurationManager.java +++ b/Emulator/src/main/java/com/eu/habbo/core/ConfigurationManager.java @@ -17,14 +17,18 @@ import java.util.Properties; public class ConfigurationManager { private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationManager.class); + private static final String EMULATOR_SETTINGS_TABLE = "emulator_settings"; + private static final String WIRED_SETTINGS_TABLE = "wired_emulator_settings"; private final Properties properties; + private final Properties wiredProperties; private final String configurationPath; public boolean loaded = false; public boolean isLoading = false; public ConfigurationManager(String configurationPath) { this.properties = new Properties(); + this.wiredProperties = new Properties(); this.configurationPath = configurationPath; this.reload(); } @@ -32,6 +36,7 @@ public class ConfigurationManager { public void reload() { this.isLoading = true; this.properties.clear(); + this.wiredProperties.clear(); InputStream input = null; @@ -87,6 +92,7 @@ public class ConfigurationManager { // Runtime envMapping.put("runtime.threads", "RT_THREADS"); envMapping.put("logging.errors.runtime", "RT_LOG_ERRORS"); + envMapping.put("hotel.timezone", "HOTEL_TIMEZONE"); for (Map.Entry entry : envMapping.entrySet()) { String envValue = System.getenv(entry.getValue()); @@ -115,31 +121,15 @@ public class ConfigurationManager { LOGGER.info("Loading configuration from database..."); long millis = System.currentTimeMillis(); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement()) { - if (statement.execute("SELECT * FROM emulator_settings")) { - try (ResultSet set = statement.getResultSet()) { - while (set.next()) { - this.properties.put(set.getString("key"), set.getString("value")); - } - } - } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } + this.loadSettingsTable(EMULATOR_SETTINGS_TABLE, this.properties, false); + this.loadSettingsTable(WIRED_SETTINGS_TABLE, this.wiredProperties, true); LOGGER.info("Configuration -> loaded! ({} MS)", System.currentTimeMillis() - millis); } public void saveToDatabase() { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE emulator_settings SET `value` = ? WHERE `key` = ? LIMIT 1")) { - for (Map.Entry entry : this.properties.entrySet()) { - statement.setString(1, entry.getValue().toString()); - statement.setString(2, entry.getKey().toString()); - statement.executeUpdate(); - } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } + this.saveSettingsTable(EMULATOR_SETTINGS_TABLE, this.properties); + this.saveSettingsTable(WIRED_SETTINGS_TABLE, this.wiredProperties); } @@ -152,10 +142,21 @@ public class ConfigurationManager { if (this.isLoading) return defaultValue; - if (!this.properties.containsKey(key)) { + Properties targetProperties = this.resolveProperties(key); + + if (targetProperties.containsKey(key)) { + return targetProperties.getProperty(key, defaultValue); + } + + if (this.isWiredSettingKey(key) && this.properties.containsKey(key)) { + return this.properties.getProperty(key, defaultValue); + } + + if (!targetProperties.containsKey(key)) { LOGGER.error("Config key not found {}", key); } - return this.properties.getProperty(key, defaultValue); + + return defaultValue; } public boolean getBoolean(String key) { @@ -208,21 +209,91 @@ public class ConfigurationManager { } public void update(String key, String value) { - this.properties.setProperty(key, value); + this.resolveProperties(key).setProperty(key, value); } public void register(String key, String value) { - if (this.properties.getProperty(key, null) != null) + this.register(key, value, ""); + } + + public void register(String key, String value, String comment) { + Properties targetProperties = this.resolveProperties(key); + + if (targetProperties.getProperty(key, null) != null) return; - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO emulator_settings VALUES (?, ?)")) { - statement.setString(1, key); - statement.setString(2, value); - statement.execute(); - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } - + this.insertSetting(key, value, comment); this.update(key, value); } + + private void loadSettingsTable(String tableName, Properties targetProperties, boolean optional) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + Statement statement = connection.createStatement()) { + if (statement.execute("SELECT * FROM " + tableName)) { + try (ResultSet set = statement.getResultSet()) { + while (set.next()) { + targetProperties.put(set.getString("key"), set.getString("value")); + } + } + } + } catch (SQLException e) { + if (optional) { + LOGGER.warn("Skipping optional config table {}: {}", tableName, e.getMessage()); + } else { + LOGGER.error("Caught SQL exception", e); + } + } + } + + private void saveSettingsTable(String tableName, Properties sourceProperties) { + String sql = "UPDATE " + tableName + " SET `value` = ? WHERE `key` = ? LIMIT 1"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement(sql)) { + for (Map.Entry entry : sourceProperties.entrySet()) { + statement.setString(1, entry.getValue().toString()); + statement.setString(2, entry.getKey().toString()); + statement.executeUpdate(); + } + } catch (SQLException e) { + if (WIRED_SETTINGS_TABLE.equals(tableName)) { + LOGGER.warn("Skipping wired config save for table {}: {}", tableName, e.getMessage()); + } else { + LOGGER.error("Caught SQL exception", e); + } + } + } + + private void insertSetting(String key, String value, String comment) { + String tableName = this.isWiredSettingKey(key) ? WIRED_SETTINGS_TABLE : EMULATOR_SETTINGS_TABLE; + String sql = this.isWiredSettingKey(key) + ? "INSERT INTO " + tableName + " (`key`, `value`, `comment`) VALUES (?, ?, ?)" + : "INSERT INTO " + tableName + " (`key`, `value`) VALUES (?, ?)"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, key); + statement.setString(2, value); + + if (this.isWiredSettingKey(key)) { + statement.setString(3, comment == null ? "" : comment); + } + + statement.execute(); + } catch (SQLException e) { + if (this.isWiredSettingKey(key)) { + LOGGER.warn("Unable to insert wired setting {} into {}: {}", key, tableName, e.getMessage()); + } else { + LOGGER.error("Caught SQL exception", e); + } + } + } + + private Properties resolveProperties(String key) { + return this.isWiredSettingKey(key) ? this.wiredProperties : this.properties; + } + + private boolean isWiredSettingKey(String key) { + return key != null && (key.startsWith("wired.") || key.startsWith("hotel.wired.")); + } } diff --git a/Emulator/src/main/java/com/eu/habbo/core/DatabaseLogger.java b/Emulator/src/main/java/com/eu/habbo/core/DatabaseLogger.java index 2f8806a9..47d8c73f 100644 --- a/Emulator/src/main/java/com/eu/habbo/core/DatabaseLogger.java +++ b/Emulator/src/main/java/com/eu/habbo/core/DatabaseLogger.java @@ -7,6 +7,10 @@ import org.slf4j.LoggerFactory; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentLinkedQueue; public class DatabaseLogger { @@ -24,19 +28,41 @@ public class DatabaseLogger { return; } - if (this.loggables.isEmpty()) { + // Drain the queue into a local snapshot so new loggables that arrive + // during this save cycle roll into the next flush instead of extending + // the current one indefinitely. + List snapshot = new ArrayList<>(); + DatabaseLoggable next; + while ((next = this.loggables.poll()) != null) { + snapshot.add(next); + } + + if (snapshot.isEmpty()) { return; } + // Group by SQL query so each distinct statement only prepares and + // executeBatches once. LinkedHashMap preserves first-seen order so + // auto-increment ids on chat/log tables correlate with the time the + // events actually happened. + Map> byQuery = new LinkedHashMap<>(); + for (DatabaseLoggable loggable : snapshot) { + byQuery.computeIfAbsent(loggable.getQuery(), k -> new ArrayList<>()).add(loggable); + } + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { - while (!this.loggables.isEmpty()) { - DatabaseLoggable loggable = this.loggables.remove(); - - try (PreparedStatement statement = connection.prepareStatement(loggable.getQuery())) { - loggable.log(statement); + for (Map.Entry> group : byQuery.entrySet()) { + List entries = group.getValue(); + try (PreparedStatement statement = connection.prepareStatement(group.getKey())) { + for (DatabaseLoggable loggable : entries) { + loggable.log(statement); + } statement.executeBatch(); + } catch (SQLException e) { + // One bad group shouldn't prevent other groups from flushing. + LOGGER.error("Exception caught while saving loggable group of size {}: {}", + entries.size(), group.getKey(), e); } - } } catch (SQLException e) { LOGGER.error("Exception caught while saving loggables to database.", e); 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 fa3a5190..73d7992a 100644 --- a/Emulator/src/main/java/com/eu/habbo/core/RoomUserPetComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/core/RoomUserPetComposer.java @@ -47,6 +47,8 @@ public class RoomUserPetComposer extends MessageComposer { this.response.appendBoolean(true); //Can breed this.response.appendInt(0); this.response.appendString(""); + this.response.appendString("unknown"); + this.response.appendInt(0); return this.response; } diff --git a/Emulator/src/main/java/com/eu/habbo/database/Database.java b/Emulator/src/main/java/com/eu/habbo/database/Database.java index 00be0fee..3866b968 100644 --- a/Emulator/src/main/java/com/eu/habbo/database/Database.java +++ b/Emulator/src/main/java/com/eu/habbo/database/Database.java @@ -4,16 +4,15 @@ import com.eu.habbo.Emulator; import com.eu.habbo.core.ConfigurationManager; import com.zaxxer.hikari.HikariDataSource; import gnu.trove.map.hash.THashMap; -import gnu.trove.set.hash.THashSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class Database { @@ -48,11 +47,9 @@ public class Database { } public void dispose() { - if (this.databasePool != null) { - this.databasePool.getDatabase().close(); + if (this.dataSource != null && !this.dataSource.isClosed()) { + this.dataSource.close(); } - - this.dataSource.close(); } public HikariDataSource getDataSource() { @@ -63,51 +60,77 @@ public class Database { return this.databasePool; } - public static PreparedStatement preparedStatementWithParams(Connection connection, String query, THashMap queryParams) throws SQLException { - THashMap params = new THashMap(); - THashSet quotedParams = new THashSet<>(); + public static PreparedStatement preparedStatementWithParams(Connection connection, + String query, + Map queryParams) throws SQLException { + StringBuilder positional = new StringBuilder(query.length()); + List bindValues = new ArrayList<>(); - for(String key : queryParams.keySet()) { - quotedParams.add(Pattern.quote(key)); - } + int i = 0; + int n = query.length(); - String regex = "(" + String.join("|", quotedParams) + ")"; + while (i < n) { + char c = query.charAt(i); - Matcher m = Pattern.compile(regex).matcher(query); - - int i = 1; - - while (m.find()) { - try { - params.put(i, queryParams.get(m.group(1))); + if (c == '\'') { + positional.append(c); i++; + while (i < n) { + char inner = query.charAt(i); + positional.append(inner); + i++; + if (inner == '\'') { + if (i < n && query.charAt(i) == '\'') { + positional.append('\''); + i++; + } else { + break; + } + } + } + continue; } - catch (Exception ignored) { } + + if (c == '@' && i + 1 < n && isNameStart(query.charAt(i + 1))) { + int start = i; + int j = i + 1; + while (j < n && isNamePart(query.charAt(j))) { + j++; + } + String name = query.substring(start, j); + if (!queryParams.containsKey(name)) { + throw new IllegalArgumentException( + "SQL template references parameter '" + name + "' but no value was provided"); + } + positional.append('?'); + bindValues.add(queryParams.get(name)); + i = j; + continue; + } + + positional.append(c); + i++; } - PreparedStatement statement = connection.prepareStatement(query.replaceAll(regex, "?")); - - for(Map.Entry set : params.entrySet()) { - if(set.getValue().getClass() == String.class) { - statement.setString(set.getKey(), (String)set.getValue()); - } - else if(set.getValue().getClass() == Integer.class) { - statement.setInt(set.getKey(), (Integer)set.getValue()); - } - else if(set.getValue().getClass() == Double.class) { - statement.setDouble(set.getKey(), (Double)set.getValue()); - } - else if(set.getValue().getClass() == Float.class) { - statement.setFloat(set.getKey(), (Float)set.getValue()); - } - else if(set.getValue().getClass() == Long.class) { - statement.setLong(set.getKey(), (Long)set.getValue()); - } - else { - statement.setObject(set.getKey(), set.getValue()); - } + PreparedStatement statement = connection.prepareStatement(positional.toString()); + for (int k = 0; k < bindValues.size(); k++) { + statement.setObject(k + 1, bindValues.get(k)); } - return statement; } + + @Deprecated + public static PreparedStatement preparedStatementWithParams(Connection connection, + String query, + THashMap queryParams) throws SQLException { + return preparedStatementWithParams(connection, query, (Map) queryParams); + } + + private static boolean isNameStart(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; + } + + private static boolean isNamePart(char c) { + return isNameStart(c) || (c >= '0' && c <= '9'); + } } diff --git a/Emulator/src/main/java/com/eu/habbo/database/DatabasePool.java b/Emulator/src/main/java/com/eu/habbo/database/DatabasePool.java index 68a3e86c..acd50764 100644 --- a/Emulator/src/main/java/com/eu/habbo/database/DatabasePool.java +++ b/Emulator/src/main/java/com/eu/habbo/database/DatabasePool.java @@ -8,41 +8,85 @@ import org.slf4j.LoggerFactory; class DatabasePool { private final Logger log = LoggerFactory.getLogger(DatabasePool.class); + + // Connection settings + private static final String DB_HOSTNAME_KEY = "db.hostname"; + private static final String DB_PORT_KEY = "db.port"; + private static final String DB_PASSWORD_KEY = "db.password"; + private static final String DB_NAME_KEY = "db.database"; + private static final String DB_USER_KEY = "db.username"; + private static final String DB_PARAMS_KEY = "db.params"; + + // Pool sizing private static final String DB_POOL_MAX_SIZE = "db.pool.maxsize"; private static final String DB_POOL_MIN_SIZE = "db.pool.minsize"; - private static final String DB_HOSTNAME_KEY = "db.hostname"; - private static final String DB_PORT_KEY = "db.port"; - private static final String DB_PASSWORD_KEY = "db.password"; - private static final String DB_NAME_KEY = "db.database"; - private static final String DB_USER_KEY = "db.username"; - private static final String DB_PARAMS_KEY = "db.params"; + + // Pool tuning (all overridable via config.ini; sensible MariaDB defaults apply otherwise) + private static final String DB_POOL_CONNECTION_TIMEOUT = "db.pool.connection_timeout_ms"; + private static final String DB_POOL_IDLE_TIMEOUT = "db.pool.idle_timeout_ms"; + private static final String DB_POOL_MAX_LIFETIME = "db.pool.max_lifetime_ms"; + private static final String DB_POOL_LEAK_THRESHOLD = "db.pool.leak_detection_ms"; + private static final String DB_POOL_VALIDATION_TIMEOUT = "db.pool.validation_timeout_ms"; + private HikariDataSource database; - private static DatabasePool instance; DatabasePool() { } - public static synchronized DatabasePool getInstance() { - if (instance == null) { - instance = new DatabasePool(); - } - return instance; - } - public boolean getStoragePooling(ConfigurationManager config) { try { HikariConfig databaseConfiguration = new HikariConfig(); + + // Pool sizing databaseConfiguration.setMaximumPoolSize(config.getInt(DB_POOL_MAX_SIZE, 50)); databaseConfiguration.setMinimumIdle(config.getInt(DB_POOL_MIN_SIZE, 10)); - databaseConfiguration.setJdbcUrl("jdbc:mysql://" + config.getValue(DB_HOSTNAME_KEY, "localhost") + ":" + config.getValue(DB_PORT_KEY, "3306") + "/" + config.getValue(DB_NAME_KEY) + config.getValue(DB_PARAMS_KEY)); - databaseConfiguration.addDataSourceProperty("serverName", config.getValue(DB_HOSTNAME_KEY, "localhost")); - databaseConfiguration.addDataSourceProperty("port", config.getValue(DB_PORT_KEY, "3306")); - databaseConfiguration.addDataSourceProperty("databaseName", config.getValue(DB_NAME_KEY)); - databaseConfiguration.addDataSourceProperty("user", config.getValue(DB_USER_KEY)); - databaseConfiguration.addDataSourceProperty("password", config.getValue(DB_PASSWORD_KEY)); + + // Pool timeouts (milliseconds) + databaseConfiguration.setConnectionTimeout(config.getInt(DB_POOL_CONNECTION_TIMEOUT, 10_000)); + databaseConfiguration.setIdleTimeout(config.getInt(DB_POOL_IDLE_TIMEOUT, 600_000)); + databaseConfiguration.setMaxLifetime(config.getInt(DB_POOL_MAX_LIFETIME, 1_800_000)); + databaseConfiguration.setValidationTimeout(config.getInt(DB_POOL_VALIDATION_TIMEOUT, 5_000)); + + // Leak detection: 0 disables it. Default 20s helps locate connections + // that weren't closed in a try-with-resources block. + int leakThreshold = config.getInt(DB_POOL_LEAK_THRESHOLD, 20_000); + if (leakThreshold > 0) { + databaseConfiguration.setLeakDetectionThreshold(leakThreshold); + } + + // Use the MariaDB Connector/J native protocol instead of the Oracle MySQL driver. + databaseConfiguration.setJdbcUrl("jdbc:mariadb://" + + config.getValue(DB_HOSTNAME_KEY, "localhost") + ":" + + config.getValue(DB_PORT_KEY, "3306") + "/" + + config.getValue(DB_NAME_KEY) + + config.getValue(DB_PARAMS_KEY)); + databaseConfiguration.setUsername(config.getValue(DB_USER_KEY)); + databaseConfiguration.setPassword(config.getValue(DB_PASSWORD_KEY)); + + // Prepared-statement caching. Without these, Hikari's cache is off entirely + // and every prepareStatement() call re-parses on the server side. + databaseConfiguration.addDataSourceProperty("cachePrepStmts", "true"); + databaseConfiguration.addDataSourceProperty("prepStmtCacheSize", "500"); + databaseConfiguration.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + databaseConfiguration.addDataSourceProperty("useServerPrepStmts", "true"); + + // Bulk write throughput: rewrites batched INSERTs into a single multi-value + // INSERT statement. Huge win for item/room/inventory persistence paths. + databaseConfiguration.addDataSourceProperty("rewriteBatchedStatements", "true"); + + // Cut per-connection round-trips. + databaseConfiguration.addDataSourceProperty("cacheServerConfiguration", "true"); + databaseConfiguration.addDataSourceProperty("useLocalSessionState", "true"); + databaseConfiguration.addDataSourceProperty("cacheResultSetMetadata", "true"); + databaseConfiguration.addDataSourceProperty("elideSetAutoCommits", "true"); + databaseConfiguration.addDataSourceProperty("maintainTimeStats", "false"); + + databaseConfiguration.setPoolName("HabboHikariPool"); + log.info("INITIALIZING DATABASE SERVER: " + config.getValue(DB_HOSTNAME_KEY)); log.info("ON PORT: " + config.getValue(DB_PORT_KEY)); log.info("HABBO DATABASE: " + config.getValue(DB_NAME_KEY)); + log.info("USING DRIVER: MariaDB Connector/J"); this.database = new HikariDataSource(databaseConfiguration); } catch (Exception e) { @@ -58,4 +102,4 @@ class DatabasePool { } return database; } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/database/SqlQueries.java b/Emulator/src/main/java/com/eu/habbo/database/SqlQueries.java new file mode 100644 index 00000000..0d0e12be --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/database/SqlQueries.java @@ -0,0 +1,113 @@ +package com.eu.habbo.database; + +import com.eu.habbo.Emulator; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public final class SqlQueries { + + private SqlQueries() { + } + + @FunctionalInterface + public interface RowMapper { + T map(ResultSet rs) throws SQLException; + } + + @FunctionalInterface + public interface RowConsumer { + void accept(ResultSet rs) throws SQLException; + } + + @FunctionalInterface + public interface ParameterBinder

{ + void bind(PreparedStatement ps, P value) throws SQLException; + } + + public static class DataAccessException extends RuntimeException { + public DataAccessException(String message, Throwable cause) { + super(message, cause); + } + } + + public static List query(String sql, RowMapper mapper, Object... params) { + try (Connection c = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement ps = c.prepareStatement(sql)) { + bindAll(ps, params); + try (ResultSet rs = ps.executeQuery()) { + List out = new ArrayList<>(); + while (rs.next()) { + out.add(mapper.map(rs)); + } + return out; + } + } catch (SQLException e) { + throw new DataAccessException("query failed: " + sql, e); + } + } + + public static Optional queryOne(String sql, RowMapper mapper, Object... params) { + try (Connection c = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement ps = c.prepareStatement(sql)) { + bindAll(ps, params); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? Optional.ofNullable(mapper.map(rs)) : Optional.empty(); + } + } catch (SQLException e) { + throw new DataAccessException("queryOne failed: " + sql, e); + } + } + + public static void forEach(String sql, RowConsumer consumer, Object... params) { + try (Connection c = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement ps = c.prepareStatement(sql)) { + bindAll(ps, params); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + consumer.accept(rs); + } + } + } catch (SQLException e) { + throw new DataAccessException("forEach failed: " + sql, e); + } + } + + public static int update(String sql, Object... params) { + try (Connection c = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement ps = c.prepareStatement(sql)) { + bindAll(ps, params); + return ps.executeUpdate(); + } catch (SQLException e) { + throw new DataAccessException("update failed: " + sql, e); + } + } + + public static

int[] batchUpdate(String sql, Collection items, ParameterBinder

binder) { + try (Connection c = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement ps = c.prepareStatement(sql)) { + for (P item : items) { + binder.bind(ps, item); + ps.addBatch(); + } + return ps.executeBatch(); + } catch (SQLException e) { + throw new DataAccessException("batchUpdate failed: " + sql, e); + } + } + + private static void bindAll(PreparedStatement ps, Object[] params) throws SQLException { + if (params == null) { + return; + } + for (int i = 0; i < params.length; i++) { + ps.setObject(i + 1, params[i]); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java index ff3700c4..36712b0e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java @@ -1,6 +1,7 @@ package com.eu.habbo.habbohotel.achievements; import com.eu.habbo.Emulator; +import com.eu.habbo.database.SqlQueries; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboBadge; @@ -49,16 +50,12 @@ public class AchievementManager { if (habbo != null) { progressAchievement(habbo, achievement, amount); } else { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); - PreparedStatement statement = connection.prepareStatement("" + - "INSERT INTO users_achievements_queue (user_id, achievement_id, amount) VALUES (?, ?, ?) " + - "ON DUPLICATE KEY UPDATE amount = amount + ?")) { - statement.setInt(1, habboId); - statement.setInt(2, achievement.id); - statement.setInt(3, amount); - statement.setInt(4, amount); - statement.execute(); - } catch (SQLException e) { + try { + SqlQueries.update( + "INSERT INTO users_achievements_queue (user_id, achievement_id, amount) VALUES (?, ?, ?) " + + "ON DUPLICATE KEY UPDATE amount = amount + ?", + habboId, achievement.id, amount, amount); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); } } @@ -203,44 +200,41 @@ public class AchievementManager { } public static void createUserEntry(Habbo habbo, Achievement achievement) { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_achievements (user_id, achievement_name, progress) VALUES (?, ?, ?)")) { - statement.setInt(1, habbo.getHabboInfo().getId()); - statement.setString(2, achievement.name); - statement.setInt(3, 1); - statement.execute(); - } catch (SQLException e) { + try { + SqlQueries.update( + "INSERT INTO users_achievements (user_id, achievement_name, progress) VALUES (?, ?, ?)", + habbo.getHabboInfo().getId(), achievement.name, 1); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); } } public static void saveAchievements(Habbo habbo) { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users_achievements SET progress = ? WHERE achievement_name = ? AND user_id = ? LIMIT 1")) { - statement.setInt(3, habbo.getHabboInfo().getId()); - for (Map.Entry map : habbo.getHabboStats().getAchievementProgress().entrySet()) { - statement.setInt(1, map.getValue()); - statement.setString(2, map.getKey().name); - statement.addBatch(); - } - statement.executeBatch(); - } catch (SQLException e) { + int userId = habbo.getHabboInfo().getId(); + try { + SqlQueries.batchUpdate( + "UPDATE users_achievements SET progress = ? WHERE achievement_name = ? AND user_id = ? LIMIT 1", + habbo.getHabboStats().getAchievementProgress().entrySet(), + (ps, entry) -> { + ps.setInt(1, entry.getValue()); + ps.setString(2, entry.getKey().name); + ps.setInt(3, userId); + }); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); } } public static int getAchievementProgressForHabbo(int userId, Achievement achievement) { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT progress FROM users_achievements WHERE user_id = ? AND achievement_name = ? LIMIT 1")) { - statement.setInt(1, userId); - statement.setString(2, achievement.name); - try (ResultSet set = statement.executeQuery()) { - if (set.next()) { - return set.getInt("progress"); - } - } - } catch (SQLException e) { + try { + return SqlQueries.queryOne( + "SELECT progress FROM users_achievements WHERE user_id = ? AND achievement_name = ? LIMIT 1", + rs -> rs.getInt("progress"), + userId, achievement.name).orElse(0); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); + return 0; } - - return 0; } public void reload() { @@ -393,4 +387,4 @@ public class AchievementManager { public TalentTrackLevel getTalentTrackLevel(TalentTrackType type, int level) { return this.talentTrackLevels.get(type).get(level); } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/campaign/calendar/CalendarManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/campaign/calendar/CalendarManager.java index 1336f88f..99c238c3 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/campaign/calendar/CalendarManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/campaign/calendar/CalendarManager.java @@ -1,6 +1,7 @@ package com.eu.habbo.habbohotel.campaign.calendar; import com.eu.habbo.Emulator; +import com.eu.habbo.database.SqlQueries; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.outgoing.events.calendar.AdventCalendarProductComposer; import com.eu.habbo.plugin.events.users.calendar.UserClaimRewardEvent; @@ -33,27 +34,27 @@ public class CalendarManager { public boolean reload() { this.dispose(); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM calendar_campaigns WHERE enabled = 1")) { - try (ResultSet set = statement.executeQuery()) { - while (set.next()) { - calendarCampaigns.put(set.getInt("id"), new CalendarCampaign(set)); - } - } - } catch (SQLException e) { + try { + SqlQueries.query( + "SELECT * FROM calendar_campaigns WHERE enabled = 1", + CalendarCampaign::new) + .forEach(c -> calendarCampaigns.put(c.getId(), c)); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); return false; } - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM calendar_rewards")) { - try (ResultSet set = statement.executeQuery()) { - while (set.next()) { - CalendarCampaign campaign = calendarCampaigns.get(set.getInt("campaign_id")); - if(campaign != null){ - campaign.addReward(new CalendarRewardObject(set)); - } - } - } - } catch (SQLException e) { + try { + SqlQueries.query( + "SELECT * FROM calendar_rewards", + rs -> Map.entry(rs.getInt("campaign_id"), new CalendarRewardObject(rs))) + .forEach(entry -> { + CalendarCampaign campaign = calendarCampaigns.get(entry.getKey()); + if (campaign != null) { + campaign.addReward(entry.getValue()); + } + }); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); return false; } @@ -94,14 +95,12 @@ public class CalendarManager { public boolean deleteCampaign(CalendarCampaign campaign) { calendarCampaigns.remove(campaign.getId()); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("DELETE FROM calendar_campaigns WHERE id = ? LIMIT 1")) { - statement.setInt(1, campaign.getId()); - return statement.execute(); - } catch (SQLException e) { + try { + return SqlQueries.update("DELETE FROM calendar_campaigns WHERE id = ? LIMIT 1", campaign.getId()) > 0; + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); + return false; } - - return false; } public CalendarCampaign getCalendarCampaign(String campaignName) { @@ -136,14 +135,15 @@ public class CalendarManager { habbo.getHabboStats().calendarRewardsClaimed.add(new CalendarRewardClaimed(habbo.getHabboInfo().getId(), campaign.getId(), day, object.getId(), new Timestamp(System.currentTimeMillis()))); habbo.getClient().sendResponse(new AdventCalendarProductComposer(true, object, habbo)); object.give(habbo); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO calendar_rewards_claimed (user_id, campaign_id, day, reward_id, timestamp) VALUES (?, ?, ?, ?, ?)")) { - statement.setInt(1, habbo.getHabboInfo().getId()); - statement.setInt(2, campaign.getId()); - statement.setInt(3, day); - statement.setInt(4, object.getId()); - statement.setInt(5, Emulator.getIntUnixTimestamp()); - statement.execute(); - } catch (SQLException e) { + try { + SqlQueries.update( + "INSERT INTO calendar_rewards_claimed (user_id, campaign_id, day, reward_id, timestamp) VALUES (?, ?, ?, ?, ?)", + habbo.getHabboInfo().getId(), + campaign.getId(), + day, + object.getId(), + Emulator.getIntUnixTimestamp()); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogItem.java index 53a979ec..7bfa22f8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogItem.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogItem.java @@ -338,6 +338,8 @@ public class CatalogItem implements ISerialize, Runnable, Comparable catalogPages; + public final TIntObjectMap buildersClubCatalogPages; public final TIntObjectMap catalogFeaturedPages; public final THashMap> prizes; public final THashMap giftWrappers; @@ -194,6 +198,7 @@ public class CatalogManager { public final THashMap targetOffers; public final THashMap clothing; public final TIntIntHashMap offerDefs; + public final TIntIntHashMap buildersClubOfferDefs; public final Item ecotronItem; public final THashMap limitedNumbers; private final List vouchers; @@ -201,6 +206,7 @@ public class CatalogManager { public CatalogManager() { long millis = System.currentTimeMillis(); this.catalogPages = TCollections.synchronizedMap(new TIntObjectHashMap<>()); + this.buildersClubCatalogPages = TCollections.synchronizedMap(new TIntObjectHashMap<>()); this.catalogFeaturedPages = new TIntObjectHashMap<>(); this.prizes = new THashMap<>(); this.giftWrappers = new THashMap<>(); @@ -210,6 +216,7 @@ public class CatalogManager { this.targetOffers = new THashMap<>(); this.clothing = new THashMap<>(); this.offerDefs = new TIntIntHashMap(); + this.buildersClubOfferDefs = new TIntIntHashMap(); this.vouchers = new ArrayList<>(); this.limitedNumbers = new THashMap<>(); @@ -226,8 +233,10 @@ public class CatalogManager { this.loadLimitedNumbers(); this.loadCatalogPages(); + this.loadBuildersClubCatalogPages(); this.loadCatalogFeaturedPages(); this.loadCatalogItems(); + this.loadBuildersClubCatalogItems(); this.loadClubOffers(); this.loadTargetOffers(); this.loadVouchers(); @@ -312,6 +321,57 @@ public class CatalogManager { LOGGER.info("Loaded {} Catalog Pages!", this.catalogPages.size()); } + private synchronized void loadBuildersClubCatalogPages() { + this.buildersClubCatalogPages.clear(); + + final THashMap pages = new THashMap<>(); + pages.put(-1, new CatalogRootLayout()); + + String query = "SELECT id, parent_id, caption, caption AS caption_save, page_layout, icon_color, icon_image, 1 AS min_rank, order_num, visible, enabled, '0' AS club_only, 'BUILDERS_CLUB' AS catalog_mode, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser, '' AS includes FROM catalog_pages_bc ORDER BY parent_id, id"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + Class pageClazz = pageDefinitions.get(set.getString("page_layout")); + + if (pageClazz == null) { + LOGGER.info("Unknown Builders Club Page Layout: {}", set.getString("page_layout")); + continue; + } + + try { + CatalogPage page = pageClazz.getConstructor(ResultSet.class).newInstance(set); + pages.put(page.getId(), page); + } catch (Exception e) { + LOGGER.error("Failed to load Builders Club layout: {}", set.getString("page_layout")); + } + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + + pages.forEachValue((object) -> { + CatalogPage page = pages.get(object.parentId); + + if (page != null) { + if (page.id != object.id) { + page.addChildPage(object); + } + } else { + if (object.parentId != -2) { + LOGGER.info("Builders Club parent page not found for {} (ID: {}, parent_id: {})", object.getPageName(), object.id, object.parentId); + } + } + return true; + }); + + this.buildersClubCatalogPages.putAll(pages); + + LOGGER.info("Loaded {} Builders Club Catalog Pages!", this.buildersClubCatalogPages.size()); + } + private synchronized void loadCatalogFeaturedPages() { this.catalogFeaturedPages.clear(); @@ -388,6 +448,53 @@ public class CatalogManager { } } + private synchronized void loadBuildersClubCatalogItems() { + this.buildersClubOfferDefs.clear(); + + String query = "SELECT id, item_ids, page_id, catalog_name, 0 AS cost_credits, 0 AS cost_points, 0 AS points_type, 1 AS amount, 0 AS limited_stack, 0 AS limited_sells, extradata, '0' AS club_only, '1' AS have_offer, id AS offer_id, order_number FROM catalog_items_bc"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + Statement statement = connection.createStatement(); + ResultSet set = statement.executeQuery(query)) { + CatalogItem item; + + while (set.next()) { + if (set.getString("item_ids").equals("0")) { + continue; + } + + CatalogPage page = this.buildersClubCatalogPages.get(set.getInt("page_id")); + + if (page == null) { + continue; + } + + item = page.getCatalogItem(set.getInt("id")); + + if (item == null) { + item = new CatalogItem(set); + page.addItem(item); + page.addOfferId(item.getOfferId()); + this.buildersClubOfferDefs.put(item.getOfferId(), item.getId()); + } else { + item.update(set); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + + for (CatalogPage page : this.buildersClubCatalogPages.valueCollection()) { + for (Integer id : page.getIncluded()) { + CatalogPage includedPage = this.buildersClubCatalogPages.get(id); + + if (includedPage != null) { + page.getCatalogItems().putAll(includedPage.getCatalogItems()); + } + } + } + } + private void loadClubOffers() { this.clubOffers.clear(); @@ -582,6 +689,10 @@ public class CatalogManager { return this.catalogPages.get(pageId); } + public CatalogPage getCatalogPage(int pageId, CatalogPageType pageType) { + return this.getCatalogPagesMap(pageType).get(pageId); + } + public CatalogPage getCatalogPage(String captionSafe) { return this.catalogPages.valueCollection().stream() .filter(p -> p != null && p.getPageName() != null && p.getPageName().equalsIgnoreCase(captionSafe)) @@ -600,9 +711,15 @@ public class CatalogManager { } public CatalogItem getCatalogItem(int id) { + return this.getCatalogItem(id, CatalogPageType.NORMAL); + } + + public CatalogItem getCatalogItem(int id, CatalogPageType pageType) { final CatalogItem[] item = {null}; - synchronized (this.catalogPages) { - this.catalogPages.forEachValue(new TObjectProcedure() { + final TIntObjectMap pagesMap = this.getCatalogPagesMap(pageType); + + synchronized (pagesMap) { + pagesMap.forEachValue(new TObjectProcedure() { @Override public boolean execute(CatalogPage object) { item[0] = object.getCatalogItem(id); @@ -617,17 +734,28 @@ public class CatalogManager { public List getCatalogPages(int parentId, final Habbo habbo) { - final List pages = new ArrayList<>(); + return this.getCatalogPages(parentId, habbo, CatalogPageType.NORMAL); + } - this.catalogPages.get(parentId).childPages.forEachValue(new TObjectProcedure() { + public List getCatalogPages(int parentId, final Habbo habbo, final CatalogPageType pageType) { + final List pages = new ArrayList<>(); + final TIntObjectMap pagesMap = this.getCatalogPagesMap(pageType); + CatalogPage parentPage = pagesMap.get(parentId); + + if (parentPage == null) { + return pages; + } + + parentPage.childPages.forEachValue(new TObjectProcedure() { @Override public boolean execute(CatalogPage object) { boolean isVisiblePage = object.visible; boolean hasRightRank = object.getRank() <= habbo.getHabboInfo().getRank().getId(); boolean clubRightsOkay = !object.isClubOnly() || habbo.getHabboInfo().getHabboStats().hasActiveClub(); + boolean pageTypeMatches = (pageType == CatalogPageType.BUILDER) || object.getCatalogPageType().matches(pageType); - if (isVisiblePage && hasRightRank && clubRightsOkay) { + if (isVisiblePage && hasRightRank && clubRightsOkay && pageTypeMatches) { pages.add(object); } return true; @@ -701,22 +829,42 @@ public class CatalogManager { } - public CatalogPage createCatalogPage(String caption, String captionSave, int roomId, int icon, CatalogPageLayouts layout, int minRank, int parentId) { + public CatalogPage createCatalogPage(String caption, String captionSave, int roomId, int icon, CatalogPageLayouts layout, int minRank, int parentId, CatalogPageType pageType, CatalogPageType catalogMode) { CatalogPage catalogPage = null; - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO catalog_pages (parent_id, caption, caption_save, icon_image, visible, enabled, min_rank, page_layout, room_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { + boolean buildersClubPage = (pageType == CatalogPageType.BUILDER); + String insertQuery = buildersClubPage + ? "INSERT INTO catalog_pages_bc (parent_id, caption, page_layout, icon_color, icon_image, order_num, visible, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" + : "INSERT INTO catalog_pages (parent_id, caption, caption_save, icon_image, visible, enabled, min_rank, page_layout, room_id, catalog_mode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + String selectQuery = buildersClubPage + ? "SELECT id, parent_id, caption, caption AS caption_save, page_layout, icon_color, icon_image, 1 AS min_rank, order_num, visible, enabled, '0' AS club_only, 'BUILDERS_CLUB' AS catalog_mode, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser, '' AS includes FROM catalog_pages_bc WHERE id = ?" + : "SELECT * FROM catalog_pages WHERE id = ?"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement(insertQuery, Statement.RETURN_GENERATED_KEYS)) { statement.setInt(1, parentId); statement.setString(2, caption); - statement.setString(3, captionSave); - statement.setInt(4, icon); - statement.setString(5, "1"); - statement.setString(6, "1"); - statement.setInt(7, minRank); - statement.setString(8, layout.name()); - statement.setInt(9, roomId); + + if (buildersClubPage) { + statement.setString(3, layout.name()); + statement.setInt(4, 1); + statement.setInt(5, icon); + statement.setInt(6, 1); + statement.setString(7, "1"); + statement.setString(8, "1"); + } else { + statement.setString(3, captionSave); + statement.setInt(4, icon); + statement.setString(5, "1"); + statement.setString(6, "1"); + statement.setInt(7, minRank); + statement.setString(8, layout.name()); + statement.setInt(9, roomId); + statement.setString(10, catalogMode.name()); + } statement.execute(); try (ResultSet set = statement.getGeneratedKeys()) { if (set.next()) { - try (PreparedStatement stmt = connection.prepareStatement("SELECT * FROM catalog_pages WHERE id = ?")) { + try (PreparedStatement stmt = connection.prepareStatement(selectQuery)) { stmt.setInt(1, set.getInt(1)); try (ResultSet page = stmt.executeQuery()) { if (page.next()) { @@ -741,7 +889,7 @@ public class CatalogManager { } if (catalogPage != null) { - this.catalogPages.put(catalogPage.getId(), catalogPage); + this.getCatalogPagesMap(pageType).put(catalogPage.getId(), catalogPage); } return catalogPage; @@ -988,7 +1136,7 @@ public class CatalogManager { if (extradata.length() > Emulator.getConfig().getInt("hotel.trophies.length.max", 300)) { extradata = extradata.substring(0, Emulator.getConfig().getInt("hotel.trophies.length.max", 300)); } - + extradata = habbo.getClient().getHabbo().getHabboInfo().getUsername() + (char) 9 + Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "-" + (Calendar.getInstance().get(Calendar.MONTH) + 1) + "-" + Calendar.getInstance().get(Calendar.YEAR) + (char) 9 + Emulator.getGameEnvironment().getWordFilter().filter(extradata.replace(((char) 9) + "", ""), habbo); } @@ -1057,7 +1205,7 @@ public class CatalogManager { if (badgeFound && item.getBaseItems().size() == 1) { habbo.getClient().sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.ALREADY_HAVE_BADGE)); - return; + return; } UserCatalogItemPurchasedEvent purchasedEvent = new UserCatalogItemPurchasedEvent(habbo, item, itemsList, totalCredits, totalPoints, badges); @@ -1141,14 +1289,23 @@ public class CatalogManager { } public List getClubOffers() { + return this.getClubOffers(ClubOffer.WINDOW_HABBO_CLUB); + } + + public TIntObjectMap getCatalogPagesMap(CatalogPageType pageType) { + return (pageType == CatalogPageType.BUILDER) ? this.buildersClubCatalogPages : this.catalogPages; + } + + public List getClubOffers(int windowId) { List offers = new ArrayList<>(); for (Map.Entry entry : this.clubOffers.entrySet()) { - if (!entry.getValue().isDeal()) { + if (!entry.getValue().isDeal() && entry.getValue().belongsToWindow(windowId)) { offers.add(entry.getValue()); } } + offers.sort(Comparator.comparingInt(ClubOffer::getId)); return offers; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPage.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPage.java index 6d36c279..0f136688 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPage.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPage.java @@ -32,6 +32,7 @@ public abstract class CatalogPage implements Comparable, ISerialize protected boolean visible; protected boolean enabled; protected boolean clubOnly; + protected CatalogPageType catalogPageType = CatalogPageType.NORMAL; protected String layout; protected String headerImage; protected String teaserImage; @@ -59,6 +60,11 @@ public abstract class CatalogPage implements Comparable, ISerialize this.visible = set.getBoolean("visible"); this.enabled = set.getBoolean("enabled"); this.clubOnly = set.getBoolean("club_only"); + try { + this.catalogPageType = CatalogPageType.fromString(set.getString("catalog_mode")); + } catch (SQLException ignored) { + this.catalogPageType = CatalogPageType.NORMAL; + } this.layout = set.getString("page_layout"); this.headerImage = set.getString("page_headline"); this.teaserImage = set.getString("page_teaser"); @@ -68,8 +74,9 @@ public abstract class CatalogPage implements Comparable, ISerialize this.textDetails = set.getString("page_text_details"); this.textTeaser = set.getString("page_text_teaser"); - if (!set.getString("includes").isEmpty()) { - for (String id : set.getString("includes").split(";")) { + String includes = set.getString("includes"); + if (includes != null && !includes.isEmpty()) { + for (String id : includes.split(";")) { try { this.included.add(Integer.valueOf(id)); } catch (Exception e) { @@ -128,6 +135,10 @@ public abstract class CatalogPage implements Comparable, ISerialize return this.clubOnly; } + public CatalogPageType getCatalogPageType() { + return this.catalogPageType; + } + public String getLayout() { return this.layout; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPageLayouts.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPageLayouts.java index ff6d01aa..f0456c1c 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPageLayouts.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPageLayouts.java @@ -43,5 +43,6 @@ public enum CatalogPageLayouts { builders_club_loyalty, monkey, niko, - mad_money + mad_money, + custom_prefix } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPageType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPageType.java index d270395f..993ea34e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPageType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogPageType.java @@ -4,6 +4,33 @@ public enum CatalogPageType { NORMAL, + BUILDER, - BUILDER + BOTH; + + public static CatalogPageType fromString(String value) { + if (value == null || value.isEmpty()) { + return NORMAL; + } + + switch (value.trim().toUpperCase()) { + case "BUILDERS_CLUB": + case "BUILDER": + case "BC": + return BUILDER; + case "BOTH": + return BOTH; + case "NORMAL": + default: + return NORMAL; + } + } + + public boolean matches(CatalogPageType requestedType) { + if (this == BOTH || requestedType == BOTH) { + return true; + } + + return this == requestedType; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/ClubOffer.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/ClubOffer.java index b7654416..3f357713 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/ClubOffer.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/ClubOffer.java @@ -10,6 +10,30 @@ import java.util.Calendar; import java.util.TimeZone; public class ClubOffer implements ISerialize { + public static final int WINDOW_HABBO_CLUB = 1; + public static final int WINDOW_BUILDERS_CLUB = 2; + public static final int WINDOW_BUILDERS_CLUB_ADDONS = 3; + + public enum OfferType { + HC, + VIP, + BUILDERS_CLUB, + BUILDERS_CLUB_ADDON; + + public static OfferType fromDatabase(String value) { + if (value == null) { + return HC; + } + + for (OfferType type : OfferType.values()) { + if (type.name().equalsIgnoreCase(value)) { + return type; + } + } + + return HC; + } + } private final int id; @@ -28,12 +52,16 @@ public class ClubOffer implements ISerialize { private final int pointsType; + private final OfferType type; + private final boolean vip; private final boolean deal; + private final boolean giftable; + public ClubOffer(ResultSet set) throws SQLException { this.id = set.getInt("id"); this.name = set.getString("name"); @@ -41,8 +69,10 @@ public class ClubOffer implements ISerialize { this.credits = set.getInt("credits"); this.points = set.getInt("points"); this.pointsType = set.getInt("points_type"); - this.vip = set.getString("type").equalsIgnoreCase("vip"); + this.type = OfferType.fromDatabase(set.getString("type")); + this.vip = this.type == OfferType.VIP; this.deal = set.getString("deal").equals("1"); + this.giftable = set.getString("giftable").equals("1"); } public int getId() { @@ -69,6 +99,10 @@ public class ClubOffer implements ISerialize { return this.pointsType; } + public OfferType getType() { + return this.type; + } + public boolean isVip() { return this.vip; } @@ -77,13 +111,49 @@ public class ClubOffer implements ISerialize { return this.deal; } + public boolean isGiftable() { + return this.giftable; + } + + public boolean isBuildersClubSubscription() { + return this.type == OfferType.BUILDERS_CLUB; + } + + public boolean isBuildersClubAddon() { + return this.type == OfferType.BUILDERS_CLUB_ADDON; + } + + public boolean isHabboClubOffer() { + return this.type == OfferType.HC || this.type == OfferType.VIP; + } + + public boolean isSubscriptionOffer() { + return !this.isBuildersClubAddon(); + } + + public int getWindowId() { + if (this.isBuildersClubAddon()) { + return WINDOW_BUILDERS_CLUB_ADDONS; + } + + if (this.isBuildersClubSubscription()) { + return WINDOW_BUILDERS_CLUB; + } + + return WINDOW_HABBO_CLUB; + } + + public boolean belongsToWindow(int windowId) { + return this.getWindowId() == windowId; + } + @Override public void serialize(ServerMessage message) { serialize(message, Emulator.getIntUnixTimestamp()); } - public void serialize(ServerMessage message, int hcExpireTimestamp) { - hcExpireTimestamp = Math.max(Emulator.getIntUnixTimestamp(), hcExpireTimestamp); + public void serialize(ServerMessage message, int expireTimestamp) { + expireTimestamp = Math.max(Emulator.getIntUnixTimestamp(), expireTimestamp); message.appendInt(this.id); message.appendString(this.name); message.appendBoolean(false); //unused @@ -96,27 +166,29 @@ public class ClubOffer implements ISerialize { long secondsTotal = seconds; - int totalYears = (int) Math.floor((int) seconds / (86400.0 * 31 * 12)); + int totalYears = (int) Math.floor(seconds / (86400.0 * 31 * 12)); seconds -= totalYears * (86400 * 31 * 12); - int totalMonths = (int) Math.floor((int) seconds / (86400.0 * 31)); + int totalMonths = (int) Math.floor(seconds / (86400.0 * 31)); seconds -= totalMonths * (86400 * 31); - int totalDays = (int) Math.floor((int) seconds / 86400.0); + int totalDays = (int) Math.floor(seconds / 86400.0); seconds -= totalDays * 86400L; - message.appendInt((int) secondsTotal / 86400 / 31); - message.appendInt((int) seconds); - message.appendBoolean(false); //giftable - message.appendInt((int) seconds); + message.appendInt(totalMonths); + message.appendInt(totalDays); + message.appendBoolean(this.giftable); + message.appendInt(totalDays); - hcExpireTimestamp += secondsTotal; + if (this.isSubscriptionOffer()) { + expireTimestamp += secondsTotal; + } Calendar cal = Calendar.getInstance(); cal.setTimeZone(TimeZone.getTimeZone("UTC")); - cal.setTimeInMillis(hcExpireTimestamp * 1000L); + cal.setTimeInMillis(expireTimestamp * 1000L); message.appendInt(cal.get(Calendar.YEAR)); message.appendInt(cal.get(Calendar.MONTH) + 1); message.appendInt(cal.get(Calendar.DAY_OF_MONTH)); } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/layouts/CustomPrefixLayout.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/layouts/CustomPrefixLayout.java new file mode 100644 index 00000000..8152b89c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/layouts/CustomPrefixLayout.java @@ -0,0 +1,27 @@ +package com.eu.habbo.habbohotel.catalog.layouts; + +import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class CustomPrefixLayout extends CatalogPage { + + public CustomPrefixLayout(ResultSet set) throws SQLException { + super(set); + } + + @Override + public void serialize(ServerMessage message) { + message.appendString("custom_prefix"); + message.appendInt(3); + message.appendString(super.getHeaderImage()); + message.appendString(super.getTeaserImage()); + message.appendString(super.getSpecialImage()); + message.appendInt(3); + message.appendString(super.getTextOne()); + message.appendString(super.getTextDetails()); + message.appendString(super.getTextTeaser()); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java index 45a7be0a..edbcd4a3 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java @@ -75,6 +75,12 @@ public class MarketPlace { return; } + ownerSet.first(); + if (ownerSet.getInt("user_id") != habbo.getHabboInfo().getId()) { + LOGGER.warn("User {} attempted to take back marketplace offer {} owned by user {}", habbo.getHabboInfo().getId(), offer.getOfferId(), ownerSet.getInt("user_id")); + return; + } + try (PreparedStatement statement = connection.prepareStatement("DELETE FROM marketplace_items WHERE id = ? AND state != 2")) { statement.setInt(1, offer.getOfferId()); int count = statement.executeUpdate(); @@ -115,10 +121,10 @@ public class MarketPlace { List offers = new ArrayList<>(10); String query = "SELECT B.* FROM marketplace_items a INNER JOIN (SELECT b.item_id AS base_item_id, b.limited_data AS ltd_data, marketplace_items.*, AVG(price) as avg, MIN(marketplace_items.price) as minPrice, MAX(marketplace_items.price) as maxPrice, COUNT(*) as number, (SELECT COUNT(*) FROM marketplace_items c INNER JOIN items as items_b ON c.item_id = items_b.id WHERE state = 2 AND items_b.item_id = base_item_id AND DATE(from_unixtime(sold_timestamp)) = CURDATE()) as sold_count_today FROM marketplace_items INNER JOIN items b ON marketplace_items.item_id = b.id INNER JOIN items_base bi ON b.item_id = bi.id INNER JOIN catalog_items ci ON bi.id = ci.item_ids WHERE price = (SELECT MIN(e.price) FROM marketplace_items e, items d WHERE e.item_id = d.id AND d.item_id = b.item_id AND e.state = 1 AND e.timestamp > ? GROUP BY d.item_id) AND state = 1 AND timestamp > ?"; if (minPrice > 0) { - query += " AND CEIL(price + (price / 100)) >= " + minPrice; + query += " AND CEIL(price + (price / 100)) >= ?"; } if (maxPrice > 0 && maxPrice > minPrice) { - query += " AND CEIL(price + (price / 100)) <= " + maxPrice; + query += " AND CEIL(price + (price / 100)) <= ?"; } if (!search.isEmpty()) { query += " AND ( bi.public_name LIKE ? OR ci.catalog_name LIKE ? ) "; @@ -155,11 +161,18 @@ public class MarketPlace { query += " LIMIT 250"; try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement(query)) { - statement.setInt(1, Emulator.getIntUnixTimestamp() - 172800); - statement.setInt(2, Emulator.getIntUnixTimestamp() - 172800); + int paramIndex = 1; + statement.setInt(paramIndex++, Emulator.getIntUnixTimestamp() - 172800); + statement.setInt(paramIndex++, Emulator.getIntUnixTimestamp() - 172800); + if (minPrice > 0) { + statement.setInt(paramIndex++, minPrice); + } + if (maxPrice > 0 && maxPrice > minPrice) { + statement.setInt(paramIndex++, maxPrice); + } if (!search.isEmpty()) { - statement.setString(3, "%" + search + "%"); - statement.setString(4, "%" + search + "%"); + statement.setString(paramIndex++, "%" + search + "%"); + statement.setString(paramIndex++, "%" + search + "%"); } try (ResultSet set = statement.executeQuery()) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/AboutCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/AboutCommand.java index 9f8e473e..3f5059f4 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/AboutCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/AboutCommand.java @@ -15,7 +15,7 @@ public class AboutCommand extends Command { } public static String credits = "Arcturus Morningstar is an opensource project based on Arcturus By TheGeneral \n" + "The Following people have all contributed to this emulator:\n" + - "TheGeneral\n Beny\n Alejandro\n Capheus\n Skeletor\n Harmonic\n Mike\n Remco\n zGrav \n Quadral \n Harmony\n Swirny\n ArpyAge\n Mikkel\n Rodolfo\n Rasmus\n Kitt Mustang\n Snaiker\n nttzx\n necmi\n Dome\n Jose Flores\n Cam\n Oliver\n Narzo\n Tenshie\n MartenM\n Ridge\n SenpaiDipper\n Snaiker\n Thijmen\n DuckieTM\n simoleo89\n Medievalshell\n Lorenzune"; + "TheGeneral\n Beny\n Alejandro\n Capheus\n Skeletor\n Harmonic\n Mike\n Remco\n zGrav \n Quadral \n Harmony\n Swirny\n ArpyAge\n Mikkel\n Rodolfo\n Rasmus\n Kitt Mustang\n Snaiker\n nttzx\n necmi\n Dome\n Jose Flores\n Cam\n Oliver\n Narzo\n Tenshie\n MartenM\n Ridge\n SenpaiDipper\n Snaiker\n Thijmen\n DuckieTM\n simoleo89\n Medievalshell\n Lorenzo (the wired master)"; @Override public boolean handle(GameClient gameClient, String[] params) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BadgeCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BadgeCommand.java index 1328ed19..8abf0b25 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BadgeCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BadgeCommand.java @@ -36,7 +36,8 @@ public class BadgeCommand extends Command { Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(params[1]); if (habbo != null) { - if (habbo.addBadge(params[2])) { + String senderName = gameClient.getHabbo().getHabboInfo().getUsername(); + if (habbo.addBadge(params[2], senderName)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_badge.given").replace("%user%", params[1]).replace("%badge%", params[2]), RoomChatMessageBubbles.ALERT); } else { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_badge.already_owned").replace("%user%", params[1]).replace("%badge%", params[2]), RoomChatMessageBubbles.ALERT); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandHandler.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandHandler.java index 956b0430..3f26738b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandHandler.java @@ -297,7 +297,11 @@ public class CommandHandler { addCommand(new SoftKickCommand()); addCommand(new SubscriptionCommand()); addCommand(new UpdateChatBubblesCommand()); - + addCommand(new GivePrefixCommand()); + addCommand(new ListPrefixesCommand()); + addCommand(new RemovePrefixCommand()); + addCommand(new PrefixBlacklistCommand()); + addCommand(new WiredCommand()); addCommand(new TestCommand()); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GivePrefixCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GivePrefixCommand.java new file mode 100644 index 00000000..eba65376 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GivePrefixCommand.java @@ -0,0 +1,66 @@ +package com.eu.habbo.habbohotel.commands; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserPrefix; +import com.eu.habbo.messages.outgoing.inventory.prefixes.PrefixReceivedComposer; +import com.eu.habbo.messages.outgoing.inventory.prefixes.UserPrefixesComposer; + +public class GivePrefixCommand extends Command { + public GivePrefixCommand() { + super("cmd_give_prefix", Emulator.getTexts().getValue("commands.keys.cmd_give_prefix").split(";")); + } + + @Override + public boolean handle(GameClient gameClient, String[] params) throws Exception { + if (params.length < 4) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_prefix.usage"), RoomChatMessageBubbles.ALERT); + return true; + } + + String targetName = params[1]; + String text = params[2]; + String color = params[3]; + String icon = params.length > 4 ? params[4] : ""; + String effect = params.length > 5 ? params[5] : ""; + + // Validate color + String[] colorParts = color.split(","); + for (String part : colorParts) { + if (!part.matches("^#[0-9A-Fa-f]{6}$")) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_prefix.invalid_color"), RoomChatMessageBubbles.ALERT); + return true; + } + } + + if (text.length() > 15) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_prefix.too_long"), RoomChatMessageBubbles.ALERT); + return true; + } + + Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetName); + + if (target == null) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_prefix.user_not_found"), RoomChatMessageBubbles.ALERT); + return true; + } + + UserPrefix prefix = new UserPrefix(target.getHabboInfo().getId(), text, color, icon, effect); + prefix.run(); + target.getInventory().getPrefixesComponent().addPrefix(prefix); + + target.getClient().sendResponse(new PrefixReceivedComposer(prefix)); + target.getClient().sendResponse(new UserPrefixesComposer(target)); + + gameClient.getHabbo().whisper( + Emulator.getTexts().getValue("commands.succes.cmd_give_prefix") + .replace("%user%", targetName) + .replace("%prefix%", text), + RoomChatMessageBubbles.ALERT + ); + + return true; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/ListPrefixesCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/ListPrefixesCommand.java new file mode 100644 index 00000000..caa78b6e --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/ListPrefixesCommand.java @@ -0,0 +1,59 @@ +package com.eu.habbo.habbohotel.commands; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserPrefix; + +import java.util.List; + +public class ListPrefixesCommand extends Command { + public ListPrefixesCommand() { + super("cmd_list_prefixes", Emulator.getTexts().getValue("commands.keys.cmd_list_prefixes").split(";")); + } + + @Override + public boolean handle(GameClient gameClient, String[] params) throws Exception { + if (params.length < 2) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_list_prefixes.usage"), RoomChatMessageBubbles.ALERT); + return true; + } + + String targetName = params[1]; + + Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetName); + + if (target == null) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_list_prefixes.user_not_found"), RoomChatMessageBubbles.ALERT); + return true; + } + + List prefixes = target.getInventory().getPrefixesComponent().getPrefixes(); + + if (prefixes.isEmpty()) { + gameClient.getHabbo().whisper( + Emulator.getTexts().getValue("commands.succes.cmd_list_prefixes.empty").replace("%user%", targetName), + RoomChatMessageBubbles.ALERT + ); + return true; + } + + StringBuilder sb = new StringBuilder(); + sb.append(Emulator.getTexts().getValue("commands.succes.cmd_list_prefixes.header").replace("%user%", targetName)).append("\r"); + + for (UserPrefix prefix : prefixes) { + sb.append("ID: ").append(prefix.getId()) + .append(" | {").append(prefix.getText()).append("}") + .append(" | Color: ").append(prefix.getColor()) + .append(prefix.getIcon().isEmpty() ? "" : " | Icon: " + prefix.getIcon()) + .append(prefix.getEffect().isEmpty() ? "" : " | Effect: " + prefix.getEffect()) + .append(prefix.isActive() ? " [ACTIVE]" : "") + .append("\r"); + } + + gameClient.getHabbo().whisper(sb.toString(), RoomChatMessageBubbles.ALERT); + + return true; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/PrefixBlacklistCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/PrefixBlacklistCommand.java new file mode 100644 index 00000000..dc8bbd69 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/PrefixBlacklistCommand.java @@ -0,0 +1,98 @@ +package com.eu.habbo.habbohotel.commands; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class PrefixBlacklistCommand extends Command { + private static final Logger LOGGER = LoggerFactory.getLogger(PrefixBlacklistCommand.class); + + public PrefixBlacklistCommand() { + super("cmd_prefix_blacklist", Emulator.getTexts().getValue("commands.keys.cmd_prefix_blacklist").split(";")); + } + + @Override + public boolean handle(GameClient gameClient, String[] params) throws Exception { + if (params.length < 2) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT); + return true; + } + + String action = params[1].toLowerCase(); + + if (action.equals("list")) { + StringBuilder sb = new StringBuilder(); + sb.append(Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.header")).append("\r"); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT word FROM custom_prefix_blacklist ORDER BY word")) { + try (ResultSet set = statement.executeQuery()) { + int count = 0; + while (set.next()) { + sb.append("- ").append(set.getString("word")).append("\r"); + count++; + } + if (count == 0) { + sb.append(Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.empty")); + } + } + } catch (SQLException e) { + LOGGER.error("Error listing prefix blacklist", e); + } + + gameClient.getHabbo().whisper(sb.toString(), RoomChatMessageBubbles.ALERT); + return true; + } + + if (params.length < 3) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT); + return true; + } + + String word = params[2].toLowerCase().trim(); + + if (word.isEmpty()) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.empty_word"), RoomChatMessageBubbles.ALERT); + return true; + } + + if (action.equals("add")) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO custom_prefix_blacklist (word) VALUES (?)")) { + statement.setString(1, word); + statement.execute(); + } catch (SQLException e) { + LOGGER.error("Error adding prefix blacklist word", e); + } + + gameClient.getHabbo().whisper( + Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.added").replace("%word%", word), + RoomChatMessageBubbles.ALERT + ); + } else if (action.equals("remove")) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM custom_prefix_blacklist WHERE word = ?")) { + statement.setString(1, word); + statement.execute(); + } catch (SQLException e) { + LOGGER.error("Error removing prefix blacklist word", e); + } + + gameClient.getHabbo().whisper( + Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.removed").replace("%word%", word), + RoomChatMessageBubbles.ALERT + ); + } else { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT); + } + + return true; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RemovePrefixCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RemovePrefixCommand.java new file mode 100644 index 00000000..9396d18d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RemovePrefixCommand.java @@ -0,0 +1,81 @@ +package com.eu.habbo.habbohotel.commands; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserPrefix; +import com.eu.habbo.messages.outgoing.inventory.prefixes.UserPrefixesComposer; + +import java.util.List; + +public class RemovePrefixCommand extends Command { + public RemovePrefixCommand() { + super("cmd_remove_prefix", Emulator.getTexts().getValue("commands.keys.cmd_remove_prefix").split(";")); + } + + @Override + public boolean handle(GameClient gameClient, String[] params) throws Exception { + if (params.length < 3) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_remove_prefix.usage"), RoomChatMessageBubbles.ALERT); + return true; + } + + String targetName = params[1]; + String prefixIdStr = params[2]; + + Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetName); + + if (target == null) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_remove_prefix.user_not_found"), RoomChatMessageBubbles.ALERT); + return true; + } + + if (prefixIdStr.equalsIgnoreCase("all")) { + List prefixes = target.getInventory().getPrefixesComponent().getPrefixes(); + for (UserPrefix prefix : prefixes) { + prefix.needsDelete(true); + Emulator.getThreading().run(prefix); + } + // Clear in-memory + for (UserPrefix prefix : prefixes) { + target.getInventory().getPrefixesComponent().removePrefix(prefix); + } + + target.getClient().sendResponse(new UserPrefixesComposer(target)); + + gameClient.getHabbo().whisper( + Emulator.getTexts().getValue("commands.succes.cmd_remove_prefix.all").replace("%user%", targetName), + RoomChatMessageBubbles.ALERT + ); + } else { + int prefixId; + try { + prefixId = Integer.parseInt(prefixIdStr); + } catch (NumberFormatException e) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_remove_prefix.invalid_id"), RoomChatMessageBubbles.ALERT); + return true; + } + + UserPrefix prefix = target.getInventory().getPrefixesComponent().getPrefix(prefixId); + + if (prefix == null) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_remove_prefix.not_found"), RoomChatMessageBubbles.ALERT); + return true; + } + + target.getInventory().getPrefixesComponent().removePrefix(prefix); + prefix.needsDelete(true); + Emulator.getThreading().run(prefix); + + target.getClient().sendResponse(new UserPrefixesComposer(target)); + + gameClient.getHabbo().whisper( + Emulator.getTexts().getValue("commands.succes.cmd_remove_prefix").replace("%user%", targetName).replace("%id%", String.valueOf(prefixId)), + RoomChatMessageBubbles.ALERT + ); + } + + return true; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RoomBundleCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RoomBundleCommand.java index e1992e98..e2776daa 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RoomBundleCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RoomBundleCommand.java @@ -4,6 +4,7 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.catalog.CatalogItem; import com.eu.habbo.habbohotel.catalog.CatalogPage; import com.eu.habbo.habbohotel.catalog.CatalogPageLayouts; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.catalog.layouts.RoomBundleLayout; import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles; @@ -41,7 +42,7 @@ public class RoomBundleCommand extends Command { points = Integer.parseInt(params[3]); pointsType = Integer.parseInt(params[4]); - CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().createCatalogPage("Room Bundle: " + gameClient.getHabbo().getHabboInfo().getCurrentRoom().getName(), "room_bundle_" + gameClient.getHabbo().getHabboInfo().getCurrentRoom().getId(), gameClient.getHabbo().getHabboInfo().getCurrentRoom().getId(), 0, CatalogPageLayouts.room_bundle, gameClient.getHabbo().getHabboInfo().getRank().getId(), parentId); + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().createCatalogPage("Room Bundle: " + gameClient.getHabbo().getHabboInfo().getCurrentRoom().getName(), "room_bundle_" + gameClient.getHabbo().getHabboInfo().getCurrentRoom().getId(), gameClient.getHabbo().getHabboInfo().getCurrentRoom().getId(), 0, CatalogPageLayouts.room_bundle, gameClient.getHabbo().getHabboInfo().getRank().getId(), parentId, CatalogPageType.NORMAL, CatalogPageType.NORMAL); if (page instanceof RoomBundleLayout) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO catalog_items (page_id, item_ids, catalog_name, cost_credits, cost_points, points_type ) VALUES (?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UnmuteCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UnmuteCommand.java index 376f8b94..7cd922e9 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UnmuteCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UnmuteCommand.java @@ -29,7 +29,7 @@ public class UnmuteCommand extends Command { } if (habbo.getHabboInfo().getCurrentRoom() != null && habbo.getHabboInfo().getCurrentRoom().isMuted(habbo)) { - habbo.getHabboInfo().getCurrentRoom().muteHabbo(habbo, 1); + habbo.getHabboInfo().getCurrentRoom().unmuteHabbo(habbo); } gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_unmute").replace("%user%", params[1]), RoomChatMessageBubbles.ALERT); @@ -41,4 +41,4 @@ public class UnmuteCommand extends Command { return true; } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UserInfoCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UserInfoCommand.java index b4a3ca78..c2a244cc 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UserInfoCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UserInfoCommand.java @@ -84,7 +84,7 @@ public class UserInfoCommand extends Command { if (onlineHabbo != null) { message.append("\r" + "Other accounts ("); - ArrayList users = Emulator.getGameEnvironment().getHabboManager().getCloneAccounts(onlineHabbo, 10); + List users = Emulator.getGameEnvironment().getHabboManager().getCloneAccounts(onlineHabbo, 10); users.sort(new Comparator() { @Override public int compare(HabboInfo o1, HabboInfo o2) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/WiredCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/WiredCommand.java new file mode 100644 index 00000000..59ab1a4b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/WiredCommand.java @@ -0,0 +1,30 @@ +package com.eu.habbo.habbohotel.commands; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles; +import com.eu.habbo.messages.outgoing.users.InClientLinkComposer; + +public class WiredCommand extends Command { + public WiredCommand() { + super(null, new String[]{"wired"}); + } + + @Override + public boolean handle(GameClient gameClient, String[] params) throws Exception { + Room room = gameClient.getHabbo().getHabboInfo().getCurrentRoom(); + + if (room == null) { + gameClient.getHabbo().whisper("You need to be inside a room to use :wired.", RoomChatMessageBubbles.ALERT); + return true; + } + + if (!room.canInspectWired(gameClient.getHabbo())) { + gameClient.sendResponse(new InClientLinkComposer("wired-tools/invalid")); + return true; + } + + gameClient.sendResponse(new InClientLinkComposer("wired-tools/show")); + return true; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java index 61c219cc..f526c15b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java @@ -26,6 +26,7 @@ public class GameClient { private Habbo habbo; private boolean handshakeFinished; private String machineId = ""; + private String ssoTicket = ""; public final ConcurrentHashMap incomingPacketCounter = new ConcurrentHashMap<>(25); public final ConcurrentHashMap, Long> messageTimestamps = new ConcurrentHashMap<>(); @@ -82,6 +83,14 @@ public class GameClient { this.machineId = machineId; } + public String getSsoTicket() { + return this.ssoTicket; + } + + public void setSsoTicket(String ssoTicket) { + this.ssoTicket = ssoTicket != null ? ssoTicket : ""; + } + public void sendResponse(MessageComposer composer) { this.sendResponse(composer.compose()); } @@ -145,8 +154,15 @@ public class GameClient { if (this.habbo != null) { if (this.habbo.isOnline()) { - this.habbo.getHabboInfo().setOnline(false); - this.habbo.disconnect(); + // Try to park the habbo in the grace period instead of immediate disconnect + boolean parked = SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket); + + if (!parked) { + // No grace period configured — immediate disconnect as before + this.habbo.getHabboInfo().setOnline(false); + this.habbo.disconnect(); + } + // If parked, do NOT call disconnect() — the habbo stays in the room } this.habbo = null; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClientManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClientManager.java index 68366a0a..cd0602cb 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClientManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClientManager.java @@ -116,6 +116,22 @@ public class GameClientManager { } + /** + * Find an existing GameClient that authenticated with the given SSO ticket. + * Used to detect reconnections where the old connection hasn't been closed yet. + */ + public GameClient findClientBySsoTicket(String ssoTicket) { + if (ssoTicket == null || ssoTicket.isEmpty()) return null; + + for (GameClient client : this.clients.values()) { + if (ssoTicket.equals(client.getSsoTicket()) && client.getHabbo() != null) { + return client; + } + } + return null; + } + + public List getHabbosWithMachineId(String machineId) { List habbos = new ArrayList<>(); 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 new file mode 100644 index 00000000..f2724578 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java @@ -0,0 +1,173 @@ +package com.eu.habbo.habbohotel.gameclients; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +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); + + private static SessionResumeManager instance; + + private final ConcurrentHashMap ghostSessions = new ConcurrentHashMap<>(); + + public static SessionResumeManager getInstance() { + if (instance == null) { + instance = new SessionResumeManager(); + } + return instance; + } + + public int getGracePeriodSeconds() { + 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 boolean parkHabbo(Habbo habbo, String ssoTicket) { + int graceSeconds = getGracePeriodSeconds(); + if (graceSeconds <= 0) { + return false; + } + + 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); + } + + 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 + ScheduledFuture future = Emulator.getThreading().run(() -> { + GhostSession ghost = ghostSessions.remove(userId); + if (ghost != null) { + LOGGER.info("[SessionResume] Grace period expired for {} (id={}) - performing full disconnect", + ghost.habbo.getHabboInfo().getUsername(), userId); + performFullDisconnect(ghost.habbo); + } + }, graceSeconds * 1000); + + ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future)); + 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); + } + + LOGGER.info("[SessionResume] Resuming session for {} (id={})", + ghost.habbo.getHabboInfo().getUsername(), userId); + + 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) { + ghost.disposeFuture.cancel(false); + } + performFullDisconnect(ghost.habbo); + } + ghostSessions.clear(); + } + + /** + * Perform the actual full disconnect that normally happens in Habbo.disconnect(). + */ + private void performFullDisconnect(Habbo habbo) { + try { + habbo.getHabboInfo().setOnline(false); + habbo.disconnect(); + } catch (Exception e) { + LOGGER.error("[SessionResume] Error during deferred disconnect", e); + } + + // Clear the SSO ticket now that the grace period is truly over + clearSsoTicket(habbo.getHabboInfo().getId()); + } + + private void restoreSsoTicket(int userId, String ssoTicket) { + try (var connection = Emulator.getDatabase().getDataSource().getConnection(); + var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) { + statement.setString(1, ssoTicket); + statement.setInt(2, userId); + statement.execute(); + LOGGER.info("[SessionResume] Restored SSO ticket for user {} during grace period", userId); + } catch (Exception e) { + LOGGER.error("[SessionResume] Failed to restore SSO ticket for user " + userId, 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")) { + statement.setString(1, ""); + statement.setInt(2, userId); + statement.execute(); + } catch (Exception e) { + LOGGER.error("[SessionResume] Failed to clear SSO ticket for user " + userId, e); + } + } + + private static class GhostSession { + final Habbo habbo; + final String ssoTicket; + final ScheduledFuture disposeFuture; + + GhostSession(Habbo habbo, String ssoTicket, ScheduledFuture disposeFuture) { + this.habbo = habbo; + this.ssoTicket = ssoTicket; + this.disposeFuture = disposeFuture; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/games/GamePlayer.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/games/GamePlayer.java index a76f01dd..114aa86d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/games/GamePlayer.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/games/GamePlayer.java @@ -36,8 +36,16 @@ public class GamePlayer { if (this.score < 0) this.score = 0; - if(isWired && this.score > 0) { + if(isWired) { this.wiredScore += amount; + + if (this.wiredScore < 0) { + this.wiredScore = 0; + } + + if (this.wiredScore > this.score) { + this.wiredScore = this.score; + } } WiredManager.triggerScoreAchieved(this.habbo.getHabboInfo().getCurrentRoom(), this.habbo.getRoomUnit(), this.score, amount); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/games/wired/WiredGame.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/games/wired/WiredGame.java index 80fe874d..631345e2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/games/wired/WiredGame.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/games/wired/WiredGame.java @@ -6,6 +6,10 @@ import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.users.Habbo; public class WiredGame extends Game { + public static final int RED_EFFECT_ID = 223; + public static final int BLUE_EFFECT_ID = 224; + public static final int YELLOW_EFFECT_ID = 225; + public static final int GREEN_EFFECT_ID = 226; public GameState state = GameState.RUNNING; public WiredGame(Room room) { @@ -28,7 +32,7 @@ public class WiredGame extends Game { @Override public boolean addHabbo(Habbo habbo, GameTeamColors teamColor) { - this.room.giveEffect(habbo, FreezeGame.effectId + teamColor.type, -1); + this.room.giveEffect(habbo, this.getEffectId(teamColor), -1); return super.addHabbo(habbo, teamColor); } @@ -47,4 +51,19 @@ public class WiredGame extends Game { public GameState getState() { return GameState.RUNNING; } -} \ No newline at end of file + + private int getEffectId(GameTeamColors teamColor) { + switch (teamColor) { + case RED: + return RED_EFFECT_ID; + case BLUE: + return BLUE_EFFECT_ID; + case YELLOW: + return YELLOW_EFFECT_ID; + case GREEN: + return GREEN_EFFECT_ID; + default: + return FreezeGame.effectId + teamColor.type; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThread.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThread.java index a82a2d01..0bc0c265 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThread.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThread.java @@ -135,16 +135,10 @@ public class ForumThread implements Runnable, ISerialize { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT A.*, B.`id` AS `last_comment_id` " + "FROM guilds_forums_threads A " + - "JOIN (" + - "SELECT * " + + "LEFT JOIN (" + + "SELECT `thread_id`, MAX(`id`) AS `id`, MAX(`created_at`) AS `created_at` " + "FROM `guilds_forums_comments` " + - "WHERE `id` IN (" + - "SELECT MAX(id) " + - "FROM `guilds_forums_comments` B " + - "GROUP BY `thread_id` AND B.`id` " + - "ORDER BY B.`id` " + - ") " + - "ORDER BY `id` DESC " + + "GROUP BY `thread_id`" + ") B ON A.`id` = B.`thread_id` " + "WHERE A.`guild_id` = ? " + "ORDER BY A.`pinned` DESC, B.`created_at` DESC " @@ -176,16 +170,10 @@ public class ForumThread implements Runnable, ISerialize { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( "SELECT A.*, B.`id` AS `last_comment_id` " + "FROM guilds_forums_threads A " + - "JOIN (" + - "SELECT * " + + "LEFT JOIN (" + + "SELECT `thread_id`, MAX(`id`) AS `id`, MAX(`created_at`) AS `created_at` " + "FROM `guilds_forums_comments` " + - "WHERE `id` IN (" + - "SELECT MAX(id) " + - "FROM `guilds_forums_comments` B " + - "GROUP BY `thread_id` AND b.`id`" + - "ORDER BY B.`id` " + - ") " + - "ORDER BY `id` DESC " + + "GROUP BY `thread_id`" + ") B ON A.`id` = B.`thread_id` " + "WHERE A.`id` = ? " + "ORDER BY A.`pinned` DESC, B.`created_at` DESC " + @@ -222,6 +210,19 @@ public class ForumThread implements Runnable, ISerialize { guildThreads.add(thread); } + public static void clearCacheForGuild(int guildId) { + synchronized (guildThreadsCache) { + THashSet threads = guildThreadsCache.remove(guildId); + if (threads != null) { + synchronized (forumThreadsCache) { + for (ForumThread thread : threads) { + forumThreadsCache.remove(thread.threadId); + } + } + } + } + } + public static void clearCache() { for (THashSet threads : guildThreadsCache.values()) { for (ForumThread thread : threads) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java index 4e0a69fe..0ca0afbe 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java @@ -91,7 +91,23 @@ public class Item implements ISerialize { this.allowGift = set.getBoolean("allow_gift"); this.allowInventoryStack = set.getBoolean("allow_inventory_stack"); - this.interactionType = Emulator.getGameEnvironment().getItemManager().getItemInteraction(set.getString("interaction_type").toLowerCase()); + String interactionTypeName = set.getString("interaction_type"); + if (interactionTypeName == null) { + interactionTypeName = "default"; + } + + this.interactionType = Emulator.getGameEnvironment().getItemManager().getItemInteraction(interactionTypeName.toLowerCase()); + + if ((this.interactionType != null) + && "default".equalsIgnoreCase(this.interactionType.getName()) + && (this.fullName != null) + && this.fullName.toLowerCase().startsWith("wf_")) { + ItemInteraction fallbackInteraction = Emulator.getGameEnvironment().getItemManager().getItemInteraction(this.fullName.toLowerCase()); + + if ((fallbackInteraction != null) && !"default".equalsIgnoreCase(fallbackInteraction.getName())) { + this.interactionType = fallbackInteraction; + } + } this.stateCount = set.getShort("interaction_modes_count"); this.effectM = set.getShort("effect_id_male"); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java index 50928f05..9280c25d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java @@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.items; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.interactions.*; import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameTimer; +import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameUpCounter; import com.eu.habbo.habbohotel.items.interactions.games.battlebanzai.InteractionBattleBanzaiPuck; import com.eu.habbo.habbohotel.items.interactions.games.battlebanzai.InteractionBattleBanzaiSphere; import com.eu.habbo.habbohotel.items.interactions.games.battlebanzai.InteractionBattleBanzaiTeleporter; @@ -48,15 +49,33 @@ import com.eu.habbo.habbohotel.items.interactions.totems.InteractionTotemLegs; import com.eu.habbo.habbohotel.items.interactions.totems.InteractionTotemPlanet; import com.eu.habbo.habbohotel.items.interactions.wired.conditions.*; import com.eu.habbo.habbohotel.items.interactions.wired.effects.*; -import com.eu.habbo.habbohotel.items.interactions.wired.selector.WiredEffectFurniArea; -import com.eu.habbo.habbohotel.items.interactions.wired.selector.WiredEffectUsersArea; -import com.eu.habbo.habbohotel.items.interactions.wired.selector.WiredEffectUsersNeighborhood; -import com.eu.habbo.habbohotel.items.interactions.wired.selector.WiredEffectFurniNeighborhood; -import com.eu.habbo.habbohotel.items.interactions.wired.selector.WiredEffectFurniByType; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredBlob; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraAnimationTime; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurni; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurniByVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUser; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUsersByVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFurniVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraMoveCarryUsers; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecuteInOrder; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecutionLimit; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraMovePhysics; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraMoveNoAnimation; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraOrEval; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRandom; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRoomVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextOutputFurniName; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextInputVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextOutputUsername; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextOutputVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraContextVariable; +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.WiredExtraUnseen; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableReference; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableLevelUpSystem; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableTextConnector; +import com.eu.habbo.habbohotel.items.interactions.wired.selector.*; import com.eu.habbo.habbohotel.items.interactions.wired.triggers.*; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboItem; @@ -150,6 +169,7 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("monsterplant_seed", InteractionMonsterPlantSeed.class)); this.interactionsList.add(new ItemInteraction("gift", InteractionGift.class)); this.interactionsList.add(new ItemInteraction("stack_helper", InteractionStackHelper.class)); + this.interactionsList.add(new ItemInteraction("stack_walk_helper", InteractionStackWalkHelper.class)); this.interactionsList.add(new ItemInteraction("puzzle_box", InteractionPuzzleBox.class)); this.interactionsList.add(new ItemInteraction("hopper", InteractionHopper.class)); this.interactionsList.add(new ItemInteraction("costume_hopper", InteractionCostumeHopper.class)); @@ -179,6 +199,13 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("youtube", InteractionYoutubeTV.class)); this.interactionsList.add(new ItemInteraction("jukebox", InteractionJukeBox.class)); this.interactionsList.add(new ItemInteraction("switch", InteractionSwitch.class)); + this.interactionsList.add(new ItemInteraction("conf_invis_control", InteractionConfInvisControl.class)); + this.interactionsList.add(new ItemInteraction("wf_conf_invis_control", InteractionConfInvisControl.class)); + this.interactionsList.add(new ItemInteraction("wf_conf_area_hide", InteractionAreaHideControl.class)); + this.interactionsList.add(new ItemInteraction("conf_area_hide", InteractionAreaHideControl.class)); + this.interactionsList.add(new ItemInteraction("wf_conf_handitem_block", InteractionHanditemBlockControl.class)); + this.interactionsList.add(new ItemInteraction("wf_conf_queue_speed", InteractionQueueSpeedControl.class)); + this.interactionsList.add(new ItemInteraction("wf_conf_wired_disable", InteractionWiredDisableControl.class)); this.interactionsList.add(new ItemInteraction("switch_remote_control", InteractionSwitchRemoteControl.class)); this.interactionsList.add(new ItemInteraction("fx_box", InteractionFXBox.class)); this.interactionsList.add(new ItemInteraction("blackhole", InteractionBlackHole.class)); @@ -202,16 +229,28 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("random_state", InteractionRandomState.class)); this.interactionsList.add(new ItemInteraction("vendingmachine_no_sides", InteractionNoSidesVendingMachine.class)); this.interactionsList.add(new ItemInteraction("tile_walkmagic", InteractionTileWalkMagic.class)); + this.interactionsList.add(new ItemInteraction("antenna", InteractionDefault.class)); + this.interactionsList.add(new ItemInteraction("room_invisible_click_tile", InteractionDefault.class)); this.interactionsList.add(new ItemInteraction("game_timer", InteractionGameTimer.class)); + this.interactionsList.add(new ItemInteraction("game_upcounter", InteractionGameUpCounter.class)); this.interactionsList.add(new ItemInteraction("wf_trg_walks_on_furni", WiredTriggerHabboWalkOnFurni.class)); this.interactionsList.add(new ItemInteraction("wf_trg_walks_off_furni", WiredTriggerHabboWalkOffFurni.class)); + this.interactionsList.add(new ItemInteraction("wf_trg_click_furni", WiredTriggerHabboClicksFurni.class)); + this.interactionsList.add(new ItemInteraction("wf_trg_click_tile", WiredTriggerHabboClicksTile.class)); + this.interactionsList.add(new ItemInteraction("wf_trg_click_user", WiredTriggerHabboClicksUser.class)); + this.interactionsList.add(new ItemInteraction("wf_trg_user_performs_action", WiredTriggerHabboPerformsAction.class)); this.interactionsList.add(new ItemInteraction("wf_trg_enter_room", WiredTriggerHabboEntersRoom.class)); + this.interactionsList.add(new ItemInteraction("wf_trg_leave_room", WiredTriggerHabboLeavesRoom.class)); this.interactionsList.add(new ItemInteraction("wf_trg_says_something", WiredTriggerHabboSaysKeyword.class)); + this.interactionsList.add(new ItemInteraction("wf_trg_clock_counter", WiredTriggerClockCounter.class)); + this.interactionsList.add(new ItemInteraction("wf_trg_var_changed", WiredTriggerVariableChanged.class)); this.interactionsList.add(new ItemInteraction("wf_trg_periodically", WiredTriggerRepeater.class)); + this.interactionsList.add(new ItemInteraction("wf_trg_period_short", WiredTriggerRepeaterShort.class)); this.interactionsList.add(new ItemInteraction("wf_trg_period_long", WiredTriggerRepeaterLong.class)); this.interactionsList.add(new ItemInteraction("wf_trg_state_changed", WiredTriggerFurniStateToggled.class)); + this.interactionsList.add(new ItemInteraction("wf_trg_stuff_state", WiredTriggerFurniStateToggled.class)); this.interactionsList.add(new ItemInteraction("wf_trg_at_given_time", WiredTriggerAtSetTime.class)); this.interactionsList.add(new ItemInteraction("wf_trg_at_time_long", WiredTriggerAtTimeLong.class)); this.interactionsList.add(new ItemInteraction("wf_trg_collision", WiredTriggerCollision.class)); @@ -242,6 +281,8 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("wf_act_move_furni_to", WiredEffectMoveFurniTo.class)); this.interactionsList.add(new ItemInteraction("wf_act_give_reward", WiredEffectGiveReward.class)); this.interactionsList.add(new ItemInteraction("wf_act_call_stacks", WiredEffectTriggerStacks.class)); + this.interactionsList.add(new ItemInteraction("wf_act_neg_call_stack", WiredEffectNegativeTriggerStacks.class)); + this.interactionsList.add(new ItemInteraction("wf_act_neg_call_stacks", WiredEffectNegativeTriggerStacks.class)); this.interactionsList.add(new ItemInteraction("wf_act_kick_user", WiredEffectKickHabbo.class)); this.interactionsList.add(new ItemInteraction("wf_act_mute_triggerer", WiredEffectMuteHabbo.class)); this.interactionsList.add(new ItemInteraction("wf_act_bot_teleport", WiredEffectBotTeleport.class)); @@ -255,12 +296,40 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("wf_act_alert", WiredEffectAlert.class)); this.interactionsList.add(new ItemInteraction("wf_act_give_handitem", WiredEffectGiveHandItem.class)); this.interactionsList.add(new ItemInteraction("wf_act_give_effect", WiredEffectGiveEffect.class)); + this.interactionsList.add(new ItemInteraction("wf_act_freeze", WiredEffectFreeze.class)); + this.interactionsList.add(new ItemInteraction("wf_act_unfreeze", WiredEffectUnfreeze.class)); + this.interactionsList.add(new ItemInteraction("wf_act_furni_to_user", WiredEffectFurniToUser.class)); + this.interactionsList.add(new ItemInteraction("wf_act_user_to_furni", WiredEffectUserToFurni.class)); + this.interactionsList.add(new ItemInteraction("wf_act_furni_to_furni", WiredEffectFurniToFurni.class)); + this.interactionsList.add(new ItemInteraction("wf_act_set_altitude", WiredEffectSetAltitude.class)); + this.interactionsList.add(new ItemInteraction("wf_act_rel_mov", WiredEffectRelativeMove.class)); + this.interactionsList.add(new ItemInteraction("wf_act_control_clock", WiredEffectControlClock.class)); + this.interactionsList.add(new ItemInteraction("wf_act_adjust_clock", WiredEffectAdjustClock.class)); + this.interactionsList.add(new ItemInteraction("wf_act_move_rotate_user", WiredEffectMoveRotateUser.class)); this.interactionsList.add(new ItemInteraction("wf_slc_furni_area", WiredEffectFurniArea.class)); this.interactionsList.add(new ItemInteraction("wf_slc_furni_neighborhood", WiredEffectFurniNeighborhood.class)); this.interactionsList.add(new ItemInteraction("wf_slc_furni_bytype", WiredEffectFurniByType.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_furni_altitude", WiredEffectFurniAltitude.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_furni_onfurni", WiredEffectFurniOnFurni.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_furni_picks", WiredEffectFurniPicks.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_furni_signal", WiredEffectFurniSignal.class)); this.interactionsList.add(new ItemInteraction("wf_slc_users_area", WiredEffectUsersArea.class)); this.interactionsList.add(new ItemInteraction("wf_slc_users_neighborhood", WiredEffectUsersNeighborhood.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_users_signal", WiredEffectUsersSignal.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_users_bytype", WiredEffectUsersByType.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_users_team", WiredEffectUsersTeam.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_users_byaction", WiredEffectUsersByAction.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_users_byname", WiredEffectUsersByName.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_users_handitem", WiredEffectUsersHandItem.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_users_onfurni", WiredEffectUsersOnFurni.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_users_group", WiredEffectUsersGroup.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_furni_with_var", WiredEffectFurniWithVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_slc_users_with_var", WiredEffectUsersWithVariable.class)); this.interactionsList.add(new ItemInteraction("wf_act_send_signal", WiredEffectSendSignal.class)); + this.interactionsList.add(new ItemInteraction("wf_act_neg_send_signal", WiredEffectNegativeSendSignal.class)); + this.interactionsList.add(new ItemInteraction("wf_act_give_var", WiredEffectGiveVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_act_remove_var", WiredEffectRemoveVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_act_change_var_val", WiredEffectChangeVariableValue.class)); this.interactionsList.add(new ItemInteraction("wf_cnd_has_furni_on", WiredConditionFurniHaveFurni.class)); this.interactionsList.add(new ItemInteraction("wf_cnd_furnis_hv_avtrs", WiredConditionFurniHaveHabbo.class)); @@ -285,14 +354,54 @@ public class ItemManager { this.interactionsList.add(new ItemInteraction("wf_cnd_actor_in_team", WiredConditionTeamMember.class)); this.interactionsList.add(new ItemInteraction("wf_cnd_trggrer_on_frn", WiredConditionTriggerOnFurni.class)); this.interactionsList.add(new ItemInteraction("wf_cnd_has_handitem", WiredConditionHabboHasHandItem.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_not_has_handitem", WiredConditionNotHabboHasHandItem.class)); this.interactionsList.add(new ItemInteraction("wf_cnd_date_rng_active", WiredConditionDateRangeActive.class)); this.interactionsList.add(new ItemInteraction("wf_cnd_valid_moves", WiredConditionMovementValidation.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_counter_time_matches", WiredConditionCounterTimeMatches.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_match_time", WiredConditionMatchTime.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_match_date", WiredConditionMatchDate.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_actor_dir", WiredConditionActorDir.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_slc_quantity", WiredConditionSelectionQuantity.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_user_performs_action", WiredConditionUserPerformsAction.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_not_user_performs_action", WiredConditionNotUserPerformsAction.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_has_altitude", WiredConditionHasAltitude.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_triggerer_match", WiredConditionTriggererMatch.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_not_triggerer_match", WiredConditionNotTriggererMatch.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_team_has_score", WiredConditionTeamHasScore.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_team_has_rank", WiredConditionTeamHasRank.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_has_var", WiredConditionHasVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_neg_has_var", WiredConditionNotHasVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_var_val_match", WiredConditionVariableValueMatch.class)); + this.interactionsList.add(new ItemInteraction("wf_cnd_var_age_match", WiredConditionVariableAgeMatch.class)); this.interactionsList.add(new ItemInteraction("wf_xtra_random", WiredExtraRandom.class)); this.interactionsList.add(new ItemInteraction("wf_xtra_unseen", WiredExtraUnseen.class)); this.interactionsList.add(new ItemInteraction("wf_blob", WiredBlob.class)); this.interactionsList.add(new ItemInteraction("wf_xtra_or_eval", WiredExtraOrEval.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_filter_furni", WiredExtraFilterFurni.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_filter_user", WiredExtraFilterUser.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_filter_users", WiredExtraFilterUser.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_filter_furni_by_var", WiredExtraFilterFurniByVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_filter_users_by_var", WiredExtraFilterUsersByVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_mov_carry_users", WiredExtraMoveCarryUsers.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_mov_no_animation", WiredExtraMoveNoAnimation.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_anim_time", WiredExtraAnimationTime.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_mov_physics", WiredExtraMovePhysics.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_exec_in_order", WiredExtraExecuteInOrder.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_execution_limit", WiredExtraExecutionLimit.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_text_output_username", WiredExtraTextOutputUsername.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_text_output_furni_name", WiredExtraTextOutputFurniName.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_text_output_variable", WiredExtraTextOutputVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_text_input_variable", WiredExtraTextInputVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_var_text_connector", WiredExtraVariableTextConnector.class)); + this.interactionsList.add(new ItemInteraction("wf_xtra_var_lvlup_system", WiredExtraVariableLevelUpSystem.class)); + this.interactionsList.add(new ItemInteraction("wf_var_user", WiredExtraUserVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_var_furni", WiredExtraFurniVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_var_room", WiredExtraRoomVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_var_context", WiredExtraContextVariable.class)); + this.interactionsList.add(new ItemInteraction("wf_var_reference", WiredExtraVariableReference.class)); + this.interactionsList.add(new ItemInteraction("wf_var_echo", WiredExtraVariableEcho.class)); this.interactionsList.add(new ItemInteraction("wf_highscore", InteractionWiredHighscore.class)); @@ -574,7 +683,7 @@ public class ItemManager { statement.execute(); try (ResultSet set = statement.getGeneratedKeys()) { - try (PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO items_presents VALUES (?, ?)")) { + try (PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO items_presents (item_id, base_item_reward) VALUES (?, ?)")) { while (set.next() && item == null) { preparedStatement.setInt(1, set.getInt(1)); preparedStatement.setInt(2, Integer.parseInt(itemId)); @@ -635,7 +744,7 @@ public class ItemManager { } public void insertTeleportPair(int itemOneId, int itemTwoId) { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO items_teleports VALUES (?, ?)")) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO items_teleports (teleport_one_id, teleport_two_id) VALUES (?, ?)")) { statement.setInt(1, itemOneId); statement.setInt(2, itemTwoId); statement.execute(); @@ -655,20 +764,28 @@ public class ItemManager { } public int[] getTargetTeleportRoomId(HabboItem item) { - int[] a = new int[]{}; + int[] target = new int[]{}; - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT items.id, items.room_id FROM items_teleports INNER JOIN items ON items_teleports.teleport_one_id = items.id OR items_teleports.teleport_two_id = items.id WHERE items.id != ? AND items.room_id > 0 LIMIT 1")) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT items_teleports.*, A.room_id as a_room_id, A.id as a_id, B.room_id as b_room_id, B.id as b_id FROM items_teleports INNER JOIN items AS A ON items_teleports.teleport_one_id = A.id INNER JOIN items AS B ON items_teleports.teleport_two_id = B.id WHERE (teleport_one_id = ? OR teleport_two_id = ?) LIMIT 1")) { statement.setInt(1, item.getId()); + statement.setInt(2, item.getId()); + try (ResultSet set = statement.executeQuery()) { if (set.next()) { - a = new int[]{set.getInt("room_id"), set.getInt("id")}; + final boolean useA = (set.getInt("a_id") != item.getId()); + final int targetRoomId = useA ? set.getInt("a_room_id") : set.getInt("b_room_id"); + final int targetItemId = useA ? set.getInt("a_id") : set.getInt("b_id"); + + if (targetRoomId > 0 && targetItemId > 0) { + target = new int[]{targetRoomId, targetItemId}; + } } } } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); } - return a; + return target; } public HabboItem loadHabboItem(int itemId) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/YoutubeManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/YoutubeManager.java index 17e2f1cf..040a13f1 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/YoutubeManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/YoutubeManager.java @@ -76,7 +76,11 @@ public class YoutubeManager { private final THashMap> playlists = new THashMap<>(); private final THashMap playlistCache = new THashMap<>(); - private final String apiKey = Emulator.getConfig().getValue("youtube.apikey"); + + private String getApiKey() { + String key = Emulator.getConfig().getValue("youtube.apikey"); + return key != null ? key : ""; + } public void load() { this.playlists.clear(); @@ -89,11 +93,19 @@ public class YoutubeManager { LOGGER.info("YouTube Manager -> Loading..."); + if (getApiKey().isEmpty()) { + LOGGER.warn("YouTube Manager -> No API key configured (youtube.apikey). YouTube TVs will not work!"); + } + + int dbEntryCount = 0; try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM youtube_playlists")) { try (ResultSet set = statement.executeQuery()) { while (set.next()) { final int itemId = set.getInt("item_id"); final String playlistId = set.getString("playlist_id"); + dbEntryCount++; + + LOGGER.info("YouTube Manager -> Loading playlist {} for base item #{}", playlistId, itemId); youtubeDataLoaderPool.submit(() -> { YoutubePlaylist playlist; @@ -101,6 +113,9 @@ public class YoutubeManager { playlist = this.getPlaylistDataById(playlistId); if (playlist != null) { this.addPlaylistToItem(itemId, playlist); + LOGGER.info("YouTube Manager -> Successfully loaded playlist {} for base item #{}", playlistId, itemId); + } else { + LOGGER.error("YouTube Manager -> Failed to load playlist {} for base item #{} (returned null - check API key and playlist ID)", playlistId, itemId); } } catch (IOException e) { LOGGER.error("Failed to load YouTube playlist {} ERROR: {}", playlistId, e); @@ -112,6 +127,10 @@ public class YoutubeManager { LOGGER.error("Caught SQL exception", e); } + if (dbEntryCount == 0) { + LOGGER.warn("YouTube Manager -> No entries found in youtube_playlists table!"); + } + youtubeDataLoaderPool.shutdown(); try { youtubeDataLoaderPool.awaitTermination(60, TimeUnit.SECONDS); @@ -125,7 +144,12 @@ public class YoutubeManager { public YoutubePlaylist getPlaylistDataById(String playlistId) throws IOException { if (this.playlistCache.containsKey(playlistId)) return this.playlistCache.get(playlistId); - if(apiKey.isEmpty()) return null; + + String apiKey = getApiKey(); + if(apiKey.isEmpty()) { + LOGGER.error("YouTube API key is not configured! Set 'youtube.apikey' in emulator_settings to enable YouTube TV."); + return null; + } YoutubePlaylist playlist; URL playlistInfo = URI.create("https://youtube.googleapis.com/youtube/v3/playlists?part=snippet&id=" + playlistId + "&maxResults=1&key=" + apiKey).toURL(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionAreaHideControl.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionAreaHideControl.java new file mode 100644 index 00000000..5c7e975b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionAreaHideControl.java @@ -0,0 +1,172 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomAreaHideSupport; +import com.eu.habbo.habbohotel.rooms.RoomLayout; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.messages.ServerMessage; +import gnu.trove.map.hash.THashMap; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionAreaHideControl extends InteractionCustomValues { + public static final THashMap defaultValues = new THashMap() { + { + this.put("state", "0"); + } + { + this.put("rootX", "0"); + } + { + this.put("rootY", "0"); + } + { + this.put("width", "0"); + } + { + this.put("length", "0"); + } + { + this.put("invisibility", "0"); + } + { + this.put("wallItems", "0"); + } + { + this.put("invert", "0"); + } + }; + + public InteractionAreaHideControl(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem, defaultValues); + this.normalizeValues(); + } + + public InteractionAreaHideControl(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells, defaultValues); + this.normalizeValues(); + } + + @Override + public void serializeExtradata(ServerMessage serverMessage) { + this.normalizeValues(); + + serverMessage.appendInt(5 + (this.isLimited() ? 256 : 0)); + serverMessage.appendInt(8); + serverMessage.appendInt(RoomAreaHideSupport.getState(this)); + serverMessage.appendInt(RoomAreaHideSupport.getRootX(this)); + serverMessage.appendInt(RoomAreaHideSupport.getRootY(this)); + serverMessage.appendInt(RoomAreaHideSupport.getWidth(this)); + serverMessage.appendInt(RoomAreaHideSupport.getLength(this)); + serverMessage.appendInt(RoomAreaHideSupport.isInvisibilityEnabled(this) ? 1 : 0); + serverMessage.appendInt(RoomAreaHideSupport.includesWallItems(this) ? 1 : 0); + serverMessage.appendInt(RoomAreaHideSupport.isInverted(this) ? 1 : 0); + + if (this.isLimited()) { + serverMessage.appendInt(this.getLimitedSells()); + serverMessage.appendInt(this.getLimitedStack()); + } + } + + @Override + public boolean isUsable() { + return true; + } + + @Override + public boolean allowWiredResetState() { + return true; + } + + @Override + public void onClick(GameClient client, Room room, Object[] objects) throws Exception { + if (room == null) { + return; + } + + boolean wiredToggle = objects != null + && objects.length >= 2 + && objects[1] instanceof WiredEffectType; + + if (!wiredToggle) { + if (client == null || !this.canToggle(client.getHabbo(), room)) { + return; + } + } + + this.values.put("state", (RoomAreaHideSupport.getState(this) == 1) ? "0" : "1"); + this.normalizeValues(); + this.needsUpdate(true); + Emulator.getThreading().run(this); + room.updateItem(this); + } + + @Override + public void onCustomValuesSaved(Room room, GameClient client, THashMap oldValues) { + this.normalizeValues(); + } + + public boolean canToggle(Habbo habbo, Room room) { + if (habbo == null || room == null) { + return false; + } + + if (room.hasRights(habbo)) { + return true; + } + + if (!habbo.getHabboStats().isRentingSpace()) { + return false; + } + + HabboItem rentedItem = room.getHabboItem(habbo.getHabboStats().rentedItemId); + + return room.getLayout() != null + && rentedItem != null + && RoomLayout.squareInSquare( + RoomLayout.getRectangle( + rentedItem.getX(), + rentedItem.getY(), + rentedItem.getBaseItem().getWidth(), + rentedItem.getBaseItem().getLength(), + rentedItem.getRotation() + ), + RoomLayout.getRectangle( + this.getX(), + this.getY(), + this.getBaseItem().getWidth(), + this.getBaseItem().getLength(), + this.getRotation() + ) + ); + } + + private void normalizeValues() { + this.values.put("state", booleanFlag(this.values.get("state"))); + this.values.put("rootX", Integer.toString(nonNegative(this.values.get("rootX")))); + this.values.put("rootY", Integer.toString(nonNegative(this.values.get("rootY")))); + this.values.put("width", Integer.toString(nonNegative(this.values.get("width")))); + this.values.put("length", Integer.toString(nonNegative(this.values.get("length")))); + this.values.put("invisibility", booleanFlag(this.values.get("invisibility"))); + this.values.put("wallItems", booleanFlag(this.values.get("wallItems"))); + this.values.put("invert", booleanFlag(this.values.get("invert"))); + } + + private static int nonNegative(String value) { + try { + return Math.max(0, Integer.parseInt(value)); + } catch (Exception ignored) { + return 0; + } + } + + private static String booleanFlag(String value) { + return ("1".equals(value) || "true".equalsIgnoreCase(value)) ? "1" : "0"; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionConfInvisControl.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionConfInvisControl.java new file mode 100644 index 00000000..fe132642 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionConfInvisControl.java @@ -0,0 +1,16 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import com.eu.habbo.habbohotel.items.Item; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionConfInvisControl extends InteractionRemoteSwitchControl { + public InteractionConfInvisControl(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public InteractionConfInvisControl(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionHanditemBlockControl.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionHanditemBlockControl.java new file mode 100644 index 00000000..59362e23 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionHanditemBlockControl.java @@ -0,0 +1,16 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import com.eu.habbo.habbohotel.items.Item; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionHanditemBlockControl extends InteractionRemoteSwitchControl { + public InteractionHanditemBlockControl(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public InteractionHanditemBlockControl(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionQueueSpeedControl.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionQueueSpeedControl.java new file mode 100644 index 00000000..6707af8d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionQueueSpeedControl.java @@ -0,0 +1,186 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.rooms.Room; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionQueueSpeedControl extends InteractionRemoteSwitchControl { + private static final int[] MODE_STATES = new int[]{0, 3, 6, 9}; + private static final int MODE_FRAME_COUNT = 3; + private static final int BASE_FRAME_DURATION_MS = 500; + + private transient volatile int animationRevision = 0; + private transient volatile int animationRoomId = 0; + private transient volatile int animationModeState = Integer.MIN_VALUE; + + public InteractionQueueSpeedControl(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public InteractionQueueSpeedControl(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + public static int toModeState(String extradata) { + int state = 0; + + try { + state = Integer.parseInt(extradata); + } catch (NumberFormatException ignored) { + } + + if (state >= 9) { + return 9; + } + + if (state >= 6) { + return 6; + } + + if (state >= 3) { + return 3; + } + + return 0; + } + + public static int toRollerSpeed(String extradata) { + int modeState = toModeState(extradata); + + if (modeState >= 9) { + return 3; + } + + if (modeState >= 6) { + return 2; + } + + if (modeState >= 3) { + return 1; + } + + return 0; + } + + public static int toRollerIntervalMs(String extradata) { + return BASE_FRAME_DURATION_MS * (toRollerSpeed(extradata) + 1); + } + + @Override + public void onClick(GameClient client, Room room, Object[] objects) throws Exception { + if (room == null) { + return; + } + + boolean wiredToggle = objects != null + && objects.length >= 2 + && objects[1] instanceof com.eu.habbo.habbohotel.wired.WiredEffectType; + + if (!wiredToggle) { + if (client == null) { + return; + } + + if (!this.canToggle(client.getHabbo(), room)) { + super.onClick(client, room, new Object[]{"QUEUE_SPEED_USE"}); + return; + } + } + + int nextModeState = getNextModeState(this.getExtradata()); + applyModeState(room, nextModeState, true); + + if (client != null) { + super.onClick(client, room, new Object[]{"TOGGLE_OVERRIDE"}); + } + } + + @Override + public void onPlace(Room room) { + super.onPlace(room); + this.ensureAnimationLoop(room); + } + + @Override + public void onPickUp(Room room) { + this.animationRevision++; + this.animationRoomId = 0; + this.animationModeState = Integer.MIN_VALUE; + super.onPickUp(room); + } + + public void ensureAnimationLoop(Room room) { + if (room == null || !room.isLoaded() || this.getRoomId() != room.getId()) { + return; + } + + int modeState = toModeState(this.getExtradata()); + + if (this.animationRoomId == room.getId() && this.animationModeState == modeState) { + return; + } + + applyModeState(room, modeState, false); + } + + private void applyModeState(Room room, int modeState, boolean persistSelection) { + if (room == null) { + return; + } + + this.animationRevision++; + this.animationRoomId = room.getId(); + this.animationModeState = modeState; + + this.setExtradata(Integer.toString(modeState)); + if (persistSelection) { + this.needsUpdate(true); + } + room.updateItemState(this); + + int revision = this.animationRevision; + int nextFrame = modeState + 1; + long delay = toRollerIntervalMs(Integer.toString(modeState)); + + Emulator.getThreading().run(() -> this.animateNextFrame(room, modeState, nextFrame, revision), delay); + } + + private void animateNextFrame(Room room, int modeState, int nextFrame, int revision) { + if (room == null || !room.isLoaded() || this.getRoomId() != room.getId()) { + return; + } + + if (revision != this.animationRevision || modeState != this.animationModeState) { + return; + } + + int maxFrame = modeState + (MODE_FRAME_COUNT - 1); + int frame = (nextFrame > maxFrame) ? modeState : nextFrame; + + this.setExtradata(Integer.toString(frame)); + room.updateItemState(this); + + int followingFrame = (frame >= maxFrame) ? modeState : (frame + 1); + long delay = toRollerIntervalMs(Integer.toString(modeState)); + + Emulator.getThreading().run(() -> this.animateNextFrame(room, modeState, followingFrame, revision), delay); + } + + private static int getNextModeState(String extradata) { + int currentModeState = toModeState(extradata); + + for (int index = 0; index < MODE_STATES.length; index++) { + if (MODE_STATES[index] != currentModeState) { + continue; + } + + return MODE_STATES[(index + 1) % MODE_STATES.length]; + } + + return MODE_STATES[0]; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRemoteSwitchControl.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRemoteSwitchControl.java new file mode 100644 index 00000000..aeb7f8f0 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRemoteSwitchControl.java @@ -0,0 +1,21 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import com.eu.habbo.habbohotel.items.Item; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionRemoteSwitchControl extends InteractionDefault { + public InteractionRemoteSwitchControl(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public InteractionRemoteSwitchControl(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean isUsable() { + return true; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionStackWalkHelper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionStackWalkHelper.java new file mode 100644 index 00000000..d6102f98 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionStackWalkHelper.java @@ -0,0 +1,48 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionStackWalkHelper extends HabboItem { + public InteractionStackWalkHelper(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public InteractionStackWalkHelper(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean canWalkOn(RoomUnit roomUnit, Room room, Object[] objects) { + return false; + } + + @Override + public boolean isWalkable() { + return false; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { + + } + + @Override + public void serializeExtradata(ServerMessage serverMessage) { + serverMessage.appendInt((this.isLimited() ? 256 : 0)); + serverMessage.appendString(this.getExtradata()); + + super.serializeExtradata(serverMessage); + } + + @Override + public boolean isUsable() { + return true; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredCondition.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredCondition.java index 97018e8b..8af6b046 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredCondition.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredCondition.java @@ -43,7 +43,7 @@ public abstract class InteractionWiredCondition extends InteractionWired impleme @Override public void onClick(GameClient client, Room room, Object[] objects) throws Exception { if (client != null) { - if (room.hasRights(client.getHabbo())) { + if (room.canInspectWired(client.getHabbo())) { client.sendResponse(new WiredConditionDataComposer(this, room)); this.activateBox(room); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredDisableControl.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredDisableControl.java new file mode 100644 index 00000000..2c69ff86 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredDisableControl.java @@ -0,0 +1,16 @@ +package com.eu.habbo.habbohotel.items.interactions; + +import com.eu.habbo.habbohotel.items.Item; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionWiredDisableControl extends InteractionRemoteSwitchControl { + public InteractionWiredDisableControl(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public InteractionWiredDisableControl(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredEffect.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredEffect.java index d303291d..a71e2820 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredEffect.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredEffect.java @@ -16,6 +16,8 @@ import com.eu.habbo.messages.outgoing.wired.WiredEffectDataComposer; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; import java.util.function.Predicate; /** @@ -78,7 +80,7 @@ public abstract class InteractionWiredEffect extends InteractionWired implements @Override public void onClick(GameClient client, Room room, Object[] objects) throws Exception { if (client != null) { - if (room.hasRights(client.getHabbo())) { + if (room.canInspectWired(client.getHabbo())) { client.sendResponse(new WiredEffectDataComposer(this, room)); this.activateBox(room); } @@ -198,4 +200,66 @@ public abstract class InteractionWiredEffect extends InteractionWired implements || additionalRemoveCondition.test(item)); return sizeBefore - items.size(); } + + protected LinkedHashSet applySelectorModifiers(Iterable matchedTargets, + Iterable availableTargets, + Iterable existingTargets, + boolean filterExisting, + boolean invert) { + LinkedHashSet matched = toLinkedHashSet(matchedTargets); + LinkedHashSet base = filterExisting + ? toLinkedHashSet(existingTargets) + : toLinkedHashSet(availableTargets); + + if (invert) { + base.removeAll(matched); + return base; + } + + if (filterExisting) { + matched.retainAll(base); + } + + return matched; + } + + protected LinkedHashSet getSelectableFloorItems(Room room) { + return this.getSelectableFloorItems(room, null); + } + + protected LinkedHashSet getSelectableFloorItems(Room room, WiredContext ctx) { + LinkedHashSet result = new LinkedHashSet<>(); + if (room == null) { + return result; + } + + boolean includeWiredItems = this.includeWiredTargets(ctx); + + room.getFloorItems().forEach(item -> { + if (item != null && (includeWiredItems || !(item instanceof InteractionWired))) { + result.add(item); + } + }); + + return result; + } + + protected boolean includeWiredTargets(WiredContext ctx) { + return ctx != null && ctx.includeWiredSelectorItems(); + } + + protected LinkedHashSet toLinkedHashSet(Iterable values) { + LinkedHashSet result = new LinkedHashSet<>(); + if (values == null) { + return result; + } + + for (T value : values) { + if (value != null) { + result.add(value); + } + } + + return result; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredExtra.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredExtra.java index b946d7d9..18ba973f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredExtra.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredExtra.java @@ -2,8 +2,11 @@ package com.eu.habbo.habbohotel.items.interactions; import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import com.eu.habbo.messages.outgoing.wired.WiredExtraDataComposer; import java.sql.ResultSet; import java.sql.SQLException; @@ -20,7 +23,10 @@ public abstract class InteractionWiredExtra extends InteractionWired { @Override public void onClick(GameClient client, Room room, Object[] objects) throws Exception { if (client != null) { - if (room.hasRights(client.getHabbo())) { + if (room.canInspectWired(client.getHabbo())) { + if (this.hasConfiguration()) { + client.sendResponse(new WiredExtraDataComposer(this, room)); + } this.activateBox(room); } } @@ -35,4 +41,12 @@ public abstract class InteractionWiredExtra extends InteractionWired { public boolean isWalkable() { return true; } -} \ No newline at end of file + + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + return true; + } + + public boolean hasConfiguration() { + return false; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredHighscore.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredHighscore.java index c02b251a..e7a79606 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredHighscore.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredHighscore.java @@ -90,6 +90,7 @@ public class InteractionWiredHighscore extends HabboItem { try { int state = Integer.parseInt(this.getExtradata()); this.setExtradata(Math.abs(state - 1) + ""); + this.needsUpdate(true); room.updateItem(this); } catch (Exception e) { LOGGER.error("Caught exception", e); @@ -150,4 +151,4 @@ public class InteractionWiredHighscore extends HabboItem { public void reloadData() { this.data = Emulator.getGameEnvironment().getItemManager().getHighscoreManager().getHighscoreRowsForItem(this.getId(), this.clearType, this.scoreType); } -} +} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredTrigger.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredTrigger.java index e6410040..24c67767 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredTrigger.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWiredTrigger.java @@ -45,7 +45,7 @@ public abstract class InteractionWiredTrigger extends InteractionWired implement @Override public void onClick(GameClient client, Room room, Object[] objects) throws Exception { if (client != null) { - if (room.hasRights(client.getHabbo())) { + if (room.canInspectWired(client.getHabbo())) { client.sendResponse(new WiredTriggerDataComposer(this, room)); this.activateBox(room); } @@ -56,6 +56,10 @@ public abstract class InteractionWiredTrigger extends InteractionWired implement public abstract boolean saveData(WiredSettings settings); + public boolean saveData(WiredSettings settings, GameClient gameClient) { + return this.saveData(settings); + } + protected int getDelay() { return this.delay; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionYoutubeTV.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionYoutubeTV.java index 9ed58fe0..2d1a0573 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionYoutubeTV.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionYoutubeTV.java @@ -56,7 +56,7 @@ public class InteractionYoutubeTV extends HabboItem { if (this.currentVideo == null) { serverMessage.appendString(""); } else { - serverMessage.appendString(Emulator.getConfig().getValue("imager.url.youtube").replace("%video%", this.currentVideo.getId())); + serverMessage.appendString("https://img.youtube.com/vi/" + this.currentVideo.getId() + "/hqdefault.jpg"); } super.serializeExtradata(serverMessage); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/games/InteractionGameTimer.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/games/InteractionGameTimer.java index e93d5308..62160f59 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/games/InteractionGameTimer.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/games/InteractionGameTimer.java @@ -24,13 +24,13 @@ import java.util.Arrays; public class InteractionGameTimer extends HabboItem { private static final Logger LOGGER = LoggerFactory.getLogger(InteractionGameTimer.class); - private int[] TIMER_INTERVAL_STEPS = new int[] { 30, 60, 120, 180, 300, 600 }; + protected int[] TIMER_INTERVAL_STEPS = new int[] { 30, 60, 120, 180, 300, 600 }; - private int baseTime = 0; - private int timeNow = 0; - private boolean isRunning = false; - private boolean isPaused = false; - private boolean threadActive = false; + protected int baseTime = 0; + protected int timeNow = 0; + protected boolean isRunning = false; + protected boolean isPaused = false; + protected boolean threadActive = false; public enum InteractionGameTimerAction { START_STOP(1), @@ -83,7 +83,7 @@ public class InteractionGameTimer extends HabboItem { parseCustomParams(item); } - private void parseCustomParams(Item baseItem) { + protected void parseCustomParams(Item baseItem) { try { TIMER_INTERVAL_STEPS = Arrays.stream(baseItem.getCustomParams().split(",")) .mapToInt(s -> { @@ -114,7 +114,7 @@ public class InteractionGameTimer extends HabboItem { } } - private void createNewGame(Room room) { + protected void createNewGame(Room room) { for(Class gameClass : Emulator.getGameEnvironment().getRoomManager().getGameTypes()) { Game existingGame = room.getGame(gameClass); @@ -132,13 +132,13 @@ public class InteractionGameTimer extends HabboItem { } } - private void pause(Room room) { + protected void pause(Room room) { for (Game game : room.getGames()) { game.pause(); } } - private void unpause(Room room) { + protected void unpause(Room room) { for (Game game : room.getGames()) { game.unpause(); } @@ -155,7 +155,8 @@ public class InteractionGameTimer extends HabboItem { public void onPickUp(Room room) { this.endGame(room); - this.setExtradata(this.baseTime + "\t" + this.baseTime); + this.timeNow = this.getInitialTimeValue(); + this.setExtradata(this.timeNow + "\t" + this.baseTime); this.needsUpdate(true); } @@ -165,7 +166,7 @@ public class InteractionGameTimer extends HabboItem { this.baseTime = this.TIMER_INTERVAL_STEPS[0]; } - this.timeNow = this.baseTime; + this.timeNow = this.getInitialTimeValue(); this.setExtradata(this.timeNow + "\t" + this.baseTime); room.updateItem(this); @@ -212,7 +213,7 @@ public class InteractionGameTimer extends HabboItem { this.createNewGame(room); - this.timeNow = this.baseTime; + this.resetTimeForStart(); this.isRunning = true; this.isPaused = false; @@ -221,7 +222,7 @@ public class InteractionGameTimer extends HabboItem { if (!this.threadActive) { this.threadActive = true; - Emulator.getThreading().run(new GameTimer(this), 1000); + this.scheduleTimerRunnable(this.getTimerStartDelayMs()); } } else if (client != null) { if (!(room.hasRights(client.getHabbo()) || client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER))) @@ -244,13 +245,13 @@ public class InteractionGameTimer extends HabboItem { if (!this.threadActive) { this.threadActive = true; - Emulator.getThreading().run(new GameTimer(this)); + this.scheduleTimerRunnable(this.getTimerResumeDelayMs()); } } } else { this.isPaused = false; this.isRunning = true; - this.timeNow = this.baseTime; + this.resetTimeForStart(); room.updateItem(this); this.createNewGame(room); @@ -258,7 +259,7 @@ public class InteractionGameTimer extends HabboItem { if (!this.threadActive) { this.threadActive = true; - Emulator.getThreading().run(new GameTimer(this), 1000); + this.scheduleTimerRunnable(this.getTimerStartDelayMs()); } } @@ -290,15 +291,15 @@ public class InteractionGameTimer extends HabboItem { if (!isRunning) { isRunning = true; isPaused = false; - if(timeNow <= 0) { - timeNow = baseTime; + if (this.shouldResetTimeOnStart()) { + this.resetTimeForStart(); room.updateItem(this); } this.createNewGame(room); WiredManager.triggerGameStarts(room); if (!threadActive) { threadActive = true; - Emulator.getThreading().run(new GameTimer(this), 1000); + this.scheduleTimerRunnable(this.getTimerStartDelayMs()); } } } @@ -322,12 +323,12 @@ public class InteractionGameTimer extends HabboItem { if (!this.threadActive) { this.threadActive = true; - Emulator.getThreading().run(new GameTimer(this), 1000); + this.scheduleTimerRunnable(this.getTimerResumeDelayMs()); } } } - private void increaseTimer(Room room) { + protected void increaseTimer(Room room) { if (this.isRunning) return; @@ -347,13 +348,45 @@ public class InteractionGameTimer extends HabboItem { } this.baseTime = baseTime; + this.timeNow = this.getInitialTimeValue(); this.setExtradata(this.timeNow + "\t" + this.baseTime); - - this.timeNow = this.baseTime; room.updateItem(this); this.needsUpdate(true); } + protected int getInitialTimeValue() { + return this.baseTime; + } + + protected boolean shouldResetTimeOnStart() { + return this.timeNow <= 0; + } + + protected void resetTimeForStart() { + this.timeNow = this.baseTime; + } + + protected Runnable createTimerRunnable() { + return new GameTimer(this); + } + + protected long getTimerStartDelayMs() { + return 1000L; + } + + protected long getTimerResumeDelayMs() { + return 0L; + } + + protected void scheduleTimerRunnable(long delayMs) { + if (delayMs <= 0) { + Emulator.getThreading().run(this.createTimerRunnable()); + return; + } + + Emulator.getThreading().run(this.createTimerRunnable(), delayMs); + } + @Override public String getDatabaseExtraData() { return this.getExtradata(); @@ -391,4 +424,8 @@ public class InteractionGameTimer extends HabboItem { public void setTimeNow(int timeNow) { this.timeNow = timeNow; } + + public int getBaseTime() { + return this.baseTime; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/games/InteractionGameUpCounter.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/games/InteractionGameUpCounter.java new file mode 100644 index 00000000..41e05a7a --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/games/InteractionGameUpCounter.java @@ -0,0 +1,214 @@ +package com.eu.habbo.habbohotel.items.interactions.games; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.threading.runnables.games.GameUpCounter; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class InteractionGameUpCounter extends InteractionGameTimer { + private static final int ONE_SECOND_MS = 1000; + private static final int HALF_SECOND_MS = 500; + private static final int MAX_UPCOUNTER_TIME = ((99 * 60) + 59); + private int subSecondOffsetMs = 0; + + public InteractionGameUpCounter(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + this.normalizeCounterState(); + } + + public InteractionGameUpCounter(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + this.normalizeCounterState(); + } + + @Override + protected void parseCustomParams(Item baseItem) { + this.TIMER_INTERVAL_STEPS = new int[] { MAX_UPCOUNTER_TIME }; + } + + private void normalizeCounterState() { + this.baseTime = MAX_UPCOUNTER_TIME; + this.setCurrentTimeInMs(this.parseStoredTime() * ONE_SECOND_MS); + this.setExtradata(this.timeNow + "\t" + this.baseTime); + } + + private int parseStoredTime() { + try { + String[] data = this.getExtradata().split("\t"); + + if (data.length > 0) { + int storedTime = Integer.parseInt(data[0]); + return Math.max(0, Math.min(storedTime, this.baseTime)); + } + } catch (Exception ignored) { + } + + return Math.max(0, Math.min(this.timeNow, this.baseTime)); + } + + @Override + protected int getInitialTimeValue() { + return 0; + } + + @Override + protected boolean shouldResetTimeOnStart() { + return this.timeNow >= this.baseTime; + } + + @Override + protected void resetTimeForStart() { + this.setCurrentTimeInMs(0); + } + + @Override + protected void increaseTimer(Room room) { + if (this.isRunning && !this.isPaused) { + return; + } + + if (this.isRunning) { + this.endGame(room); + WiredManager.triggerGameEnds(room); + } + + this.baseTime = MAX_UPCOUNTER_TIME; + this.setCurrentTimeInMs(0); + this.applyCounterState(room, true); + } + + @Override + protected Runnable createTimerRunnable() { + return new GameUpCounter(this); + } + + @Override + protected long getTimerStartDelayMs() { + return this.getNextTickDelayMs(); + } + + @Override + protected long getTimerResumeDelayMs() { + return this.getNextTickDelayMs(); + } + + public int getCurrentTimeInMs() { + return (this.timeNow * ONE_SECOND_MS) + this.subSecondOffsetMs; + } + + public int getMaximumTimeInMs() { + return this.baseTime * ONE_SECOND_MS; + } + + public long getNextTickDelayMs() { + return (this.subSecondOffsetMs > 0) ? HALF_SECOND_MS : ONE_SECOND_MS; + } + + public void setCurrentTimeInMs(int totalMs) { + int clamped = Math.max(0, Math.min(totalMs, this.getMaximumTimeInMs())); + int remainder = clamped % ONE_SECOND_MS; + + this.timeNow = (clamped / ONE_SECOND_MS); + this.subSecondOffsetMs = (remainder >= HALF_SECOND_MS) ? HALF_SECOND_MS : 0; + } + + public void advanceCounterInMs(int deltaMs) { + this.setCurrentTimeInMs(this.getCurrentTimeInMs() + deltaMs); + } + + private void applyCounterState(Room room, boolean updateRoom) { + this.setExtradata(this.timeNow + "\t" + this.baseTime); + + if (updateRoom && room != null) { + room.updateItem(this); + } + + this.needsUpdate(true); + } + + public void restartFromZero(Room room) { + boolean wasActive = this.isRunning || this.isPaused; + + if (wasActive) { + this.endGame(room); + WiredManager.triggerGameEnds(room); + } + + this.setCurrentTimeInMs(0); + this.applyCounterState(room, true); + + this.startTimer(room); + } + + public void stopCounter(Room room) { + boolean wasActive = this.isRunning || this.isPaused; + + this.endGame(room); + this.applyCounterState(room, true); + + if (wasActive) { + WiredManager.triggerGameEnds(room); + } + } + + public void resetCounter(Room room) { + boolean wasActive = this.isRunning || this.isPaused; + + this.endGame(room); + this.setCurrentTimeInMs(0); + this.applyCounterState(room, true); + + if (wasActive) { + WiredManager.triggerGameEnds(room); + } + } + + public void pauseCounter(Room room) { + if (!this.isRunning || this.isPaused) { + return; + } + + this.pauseTimer(room); + this.applyCounterState(room, true); + } + + public void resumeCounter(Room room) { + if (!this.isPaused) { + return; + } + + this.resumeTimer(room); + this.applyCounterState(room, true); + } + + public void adjustCounter(Room room, int operator, int minutes, int halfSecondSteps) { + int deltaMs = (Math.max(0, minutes) * 60000) + (Math.max(0, halfSecondSteps) * HALF_SECOND_MS); + int nextTimeMs = this.getCurrentTimeInMs(); + + switch (operator) { + case 0: + nextTimeMs += deltaMs; + break; + case 1: + nextTimeMs -= deltaMs; + break; + case 2: + default: + nextTimeMs = deltaMs; + break; + } + + this.setCurrentTimeInMs(nextTimeMs); + this.applyCounterState(room, true); + } + + public void resetOnRoomUnload(Room room) { + this.endGame(room); + this.setThreadActive(false); + this.setCurrentTimeInMs(0); + this.applyCounterState(null, false); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionActorDir.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionActorDir.java new file mode 100644 index 00000000..a9670b2e --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionActorDir.java @@ -0,0 +1,193 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +public class WiredConditionActorDir extends InteractionWiredCondition { + private static final int QUANTIFIER_ALL = 0; + private static final int QUANTIFIER_ANY = 1; + private static final int ALL_DIRECTIONS_MASK = createDirectionMask(); + + public static final WiredConditionType type = WiredConditionType.ACTOR_DIR; + + private int directionMask = 0; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int quantifier = QUANTIFIER_ALL; + + public WiredConditionActorDir(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionActorDir(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(3); + message.appendInt(this.directionMask); + message.appendInt(this.userSource); + message.appendInt(this.quantifier); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] params = settings.getIntParams(); + + this.directionMask = (params.length > 0) ? this.normalizeDirectionMask(params[0]) : 0; + this.userSource = (params.length > 1) ? this.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL; + + return true; + } + + @Override + public boolean evaluate(WiredContext ctx) { + if (this.directionMask == 0) { + return false; + } + + List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); + if (targets.isEmpty()) { + return false; + } + + if (this.quantifier == QUANTIFIER_ANY) { + return targets.stream().anyMatch(this::matchesDirection); + } + + return targets.stream().allMatch(this::matchesDirection); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.directionMask, + this.userSource, + this.quantifier + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data == null) { + return; + } + + this.directionMask = this.normalizeDirectionMask(data.directionMask); + this.userSource = this.normalizeUserSource(data.userSource); + this.quantifier = this.normalizeQuantifier(data.quantifier); + return; + } + + String[] parts = wiredData.split("\t"); + + try { + if (parts.length > 0) { + this.directionMask = this.normalizeDirectionMask(Integer.parseInt(parts[0])); + } + if (parts.length > 1) { + this.userSource = this.normalizeUserSource(Integer.parseInt(parts[1])); + } + if (parts.length > 2) { + this.quantifier = this.normalizeQuantifier(Integer.parseInt(parts[2])); + } + } catch (NumberFormatException ignored) { + this.onPickUp(); + } + } + + @Override + public void onPickUp() { + this.directionMask = 0; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; + } + + private boolean matchesDirection(RoomUnit roomUnit) { + if (roomUnit == null || roomUnit.getBodyRotation() == null) { + return false; + } + + int direction = roomUnit.getBodyRotation().getValue(); + + return (this.directionMask & (1 << direction)) != 0; + } + + private int normalizeDirectionMask(int value) { + return value & ALL_DIRECTIONS_MASK; + } + + private int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private int normalizeQuantifier(int value) { + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + + private static int createDirectionMask() { + int mask = 0; + + for (int direction = 0; direction < 8; direction++) { + mask |= (1 << direction); + } + + return mask; + } + + static class JsonData { + int directionMask; + int userSource; + int quantifier; + + public JsonData(int directionMask, int userSource, int quantifier) { + this.directionMask = directionMask; + this.userSource = userSource; + this.quantifier = quantifier; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionCounterTimeMatches.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionCounterTimeMatches.java new file mode 100644 index 00000000..a28ea00b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionCounterTimeMatches.java @@ -0,0 +1,296 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; +import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameUpCounter; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import gnu.trove.set.hash.THashSet; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.stream.Collectors; + +public class WiredConditionCounterTimeMatches extends InteractionWiredCondition { + private static final int COMPARISON_LESS = 0; + private static final int COMPARISON_EQUAL = 1; + private static final int COMPARISON_GREATER = 2; + private static final int QUANTIFIER_ALL = 0; + private static final int QUANTIFIER_ANY = 1; + private static final int MAX_MINUTES = 99; + private static final int MAX_HALF_SECOND_STEPS = 119; + + public static final WiredConditionType type = WiredConditionType.COUNTER_TIME_MATCHES; + + private final THashSet items; + private int comparison = COMPARISON_EQUAL; + private int minutes = 0; + private int halfSecondSteps = 0; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private int quantifier = QUANTIFIER_ALL; + + public WiredConditionCounterTimeMatches(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + this.items = new THashSet<>(); + } + + public WiredConditionCounterTimeMatches(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + this.items = new THashSet<>(); + } + + @Override + public boolean evaluate(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) { + return false; + } + + this.refresh(room); + + List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + if (targets.isEmpty()) { + return false; + } + + int targetTimeInMs = this.getTargetTimeInMs(); + + if (this.quantifier == QUANTIFIER_ANY) { + for (HabboItem item : targets) { + if (!(item instanceof InteractionGameUpCounter)) { + continue; + } + + if (this.matchesCounter((InteractionGameUpCounter) item, targetTimeInMs)) { + return true; + } + } + + return false; + } + + for (HabboItem item : targets) { + if (!(item instanceof InteractionGameUpCounter)) { + return false; + } + + if (!this.matchesCounter((InteractionGameUpCounter) item, targetTimeInMs)) { + return false; + } + } + + return true; + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.comparison, + this.minutes, + this.halfSecondSteps, + this.furniSource, + this.quantifier, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.items.clear(); + this.comparison = COMPARISON_EQUAL; + this.minutes = 0; + this.halfSecondSteps = 0; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.comparison = this.normalizeComparison(data.comparison); + this.minutes = this.normalizeMinutes(data.minutes); + this.halfSecondSteps = this.normalizeHalfSecondSteps(data.halfSecondSteps); + this.furniSource = data.furniSource; + this.quantifier = this.normalizeQuantifier(data.quantifier); + + if (data.itemIds == null) { + return; + } + + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + if (item instanceof InteractionGameUpCounter) { + this.items.add(item); + } + } + } + + @Override + public void onPickUp() { + this.items.clear(); + this.comparison = COMPARISON_EQUAL; + this.minutes = 0; + this.halfSecondSteps = 0; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(room); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(this.items.size()); + + for (HabboItem item : this.items) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(5); + message.appendInt(this.comparison); + message.appendInt(this.minutes); + message.appendInt(this.halfSecondSteps); + message.appendInt(this.furniSource); + message.appendInt(this.quantifier); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] params = settings.getIntParams(); + + this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL; + this.minutes = (params.length > 1) ? this.normalizeMinutes(params[1]) : 0; + this.halfSecondSteps = (params.length > 2) ? this.normalizeHalfSecondSteps(params[2]) : 0; + this.furniSource = (params.length > 3) ? params[3] : WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = (params.length > 4) ? this.normalizeQuantifier(params[4]) : QUANTIFIER_ALL; + + this.items.clear(); + + if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { + return true; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + return false; + } + + int count = settings.getFurniIds().length; + if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + return false; + } + + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + if (item instanceof InteractionGameUpCounter) { + this.items.add(item); + } + } + + return true; + } + + private void refresh(Room room) { + THashSet remove = new THashSet<>(); + + for (HabboItem item : this.items) { + HabboItem roomItem = room.getHabboItem(item.getId()); + if (!(roomItem instanceof InteractionGameUpCounter)) { + remove.add(item); + } + } + + for (HabboItem item : remove) { + this.items.remove(item); + } + } + + private int getTargetTimeInMs() { + return (this.minutes * 60_000) + (this.halfSecondSteps * 500); + } + + private boolean matchesCounter(InteractionGameUpCounter counter, int targetTimeInMs) { + int currentTimeInMs = counter.getCurrentTimeInMs(); + + switch (this.comparison) { + case COMPARISON_LESS: + return currentTimeInMs < targetTimeInMs; + case COMPARISON_GREATER: + return currentTimeInMs > targetTimeInMs; + default: + return currentTimeInMs == targetTimeInMs; + } + } + + private int normalizeComparison(int value) { + if (value < COMPARISON_LESS || value > COMPARISON_GREATER) { + return COMPARISON_EQUAL; + } + + return value; + } + + private int normalizeMinutes(int value) { + return Math.max(0, Math.min(MAX_MINUTES, value)); + } + + private int normalizeHalfSecondSteps(int value) { + return Math.max(0, Math.min(MAX_HALF_SECOND_STEPS, value)); + } + + private int normalizeQuantifier(int value) { + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + + static class JsonData { + int comparison; + int minutes; + int halfSecondSteps; + int furniSource; + int quantifier; + List itemIds; + + public JsonData(int comparison, int minutes, int halfSecondSteps, int furniSource, int quantifier, List itemIds) { + this.comparison = comparison; + this.minutes = minutes; + this.halfSecondSteps = halfSecondSteps; + this.furniSource = furniSource; + this.quantifier = quantifier; + this.itemIds = itemIds; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveFurni.java index 1e96dfc3..01eae2ad 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveFurni.java @@ -177,6 +177,10 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition { int count = settings.getFurniIds().length; if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; + if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + this.items.clear(); if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveHabbo.java index 1863c07c..4fb60898 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveHabbo.java @@ -27,6 +27,7 @@ import java.util.stream.Collectors; public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { public static final WiredConditionType type = WiredConditionType.FURNI_HAVE_HABBO; protected THashSet items; + protected boolean all; private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; public WiredConditionFurniHaveHabbo(ResultSet set, Item baseItem) throws SQLException { @@ -42,6 +43,7 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { @Override public void onPickUp() { this.items.clear(); + this.all = false; this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; } @@ -62,15 +64,11 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { Collection bots = room.getCurrentBots().valueCollection(); Collection pets = room.getCurrentPets().valueCollection(); - return targets.stream().filter(item -> item != null).allMatch(item -> { - RoomTile baseTile = room.getLayout().getTile(item.getX(), item.getY()); - if (baseTile == null) return false; - - THashSet occupiedTiles = room.getLayout().getTilesAt(baseTile, item.getBaseItem().getWidth(), item.getBaseItem().getLength(), item.getRotation()); - return habbos.stream().anyMatch(character -> character.getRoomUnit() != null && occupiedTiles.contains(character.getRoomUnit().getCurrentLocation())) || - bots.stream().anyMatch(character -> character.getRoomUnit() != null && occupiedTiles.contains(character.getRoomUnit().getCurrentLocation())) || - pets.stream().anyMatch(character -> character.getRoomUnit() != null && occupiedTiles.contains(character.getRoomUnit().getCurrentLocation())); - }); + if (this.all) { + return targets.stream().filter(item -> item != null).allMatch(item -> this.hasAvatarOnItem(item, room, habbos, bots, pets)); + } + + return targets.stream().filter(item -> item != null).anyMatch(item -> this.hasAvatarOnItem(item, room, habbos, bots, pets)); } @Deprecated @@ -84,7 +82,8 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { this.refresh(); return WiredManager.getGson().toJson(new JsonData( this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), - this.furniSource + this.furniSource, + this.all )); } @@ -96,6 +95,7 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.furniSource = data.furniSource; + this.all = data.all; for(int id : data.itemIds) { HabboItem item = room.getHabboItem(id); @@ -119,6 +119,7 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { } } this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; + this.all = false; } if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { this.furniSource = WiredSourceUtil.SOURCE_SELECTED; @@ -144,7 +145,8 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(1); + message.appendInt(2); + message.appendInt(this.all ? 1 : 0); message.appendInt(this.furniSource); message.appendInt(0); message.appendInt(this.getType().code); @@ -159,7 +161,12 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; int[] params = settings.getIntParams(); - this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.all = (params.length > 0) && (params[0] == 1); + this.furniSource = (params.length > 1) ? params[1] : ((params.length > 0 && params[0] > 1) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER); + + if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } this.items.clear(); @@ -179,6 +186,18 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { return true; } + protected boolean hasAvatarOnItem(HabboItem item, Room room, Collection habbos, Collection bots, Collection pets) { + RoomTile baseTile = room.getLayout().getTile(item.getX(), item.getY()); + if (baseTile == null) return false; + + THashSet occupiedTiles = room.getLayout().getTilesAt(baseTile, item.getBaseItem().getWidth(), item.getBaseItem().getLength(), item.getRotation()); + return occupiedTiles != null && ( + habbos.stream().anyMatch(character -> character.getRoomUnit() != null && occupiedTiles.contains(character.getRoomUnit().getCurrentLocation())) || + bots.stream().anyMatch(character -> character.getRoomUnit() != null && occupiedTiles.contains(character.getRoomUnit().getCurrentLocation())) || + pets.stream().anyMatch(character -> character.getRoomUnit() != null && occupiedTiles.contains(character.getRoomUnit().getCurrentLocation())) + ); + } + private void refresh() { THashSet items = new THashSet<>(); @@ -200,10 +219,12 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { static class JsonData { List itemIds; int furniSource; + boolean all; - public JsonData(List itemIds, int furniSource) { + public JsonData(List itemIds, int furniSource, boolean all) { this.itemIds = itemIds; this.furniSource = furniSource; + this.all = all; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniTypeMatch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniTypeMatch.java index 72552354..02d2a0c9 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniTypeMatch.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniTypeMatch.java @@ -8,22 +8,30 @@ import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredConditionType; -import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; import gnu.trove.set.hash.THashSet; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; public class WiredConditionFurniTypeMatch extends InteractionWiredCondition { + protected static final int SOURCE_SECONDARY_SELECTED = 101; + protected static final int QUANTIFIER_ALL = 0; + protected static final int QUANTIFIER_ANY = 1; + public static final WiredConditionType type = WiredConditionType.STUFF_IS; - private THashSet items = new THashSet<>(); - private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + protected THashSet items = new THashSet<>(); + protected THashSet secondaryItems = new THashSet<>(); + protected int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int compareFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int quantifier = QUANTIFIER_ALL; public WiredConditionFurniTypeMatch(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -36,19 +44,90 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition { @Override public void onPickUp() { this.items.clear(); + this.secondaryItems.clear(); this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.compareFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; } @Override public boolean evaluate(WiredContext ctx) { + if (this.quantifier == QUANTIFIER_ANY) { + return this.evaluateAnyMatches(ctx); + } + + return this.evaluateAllMatches(ctx); + } + + protected boolean evaluateAllMatches(WiredContext ctx) { + List matchTargets = this.resolveMatchTargets(ctx); + if (matchTargets.isEmpty()) { + return false; + } + + THashSet compareTypeIds = this.resolveCompareTypeIds(ctx); + if (compareTypeIds.isEmpty()) { + return false; + } + + for (HabboItem item : matchTargets) { + if (!this.matchesType(item, compareTypeIds)) { + return false; + } + } + + return true; + } + + protected boolean evaluateAnyMatches(WiredContext ctx) { + List matchTargets = this.resolveMatchTargets(ctx); + if (matchTargets.isEmpty()) { + return false; + } + + THashSet compareTypeIds = this.resolveCompareTypeIds(ctx); + if (compareTypeIds.isEmpty()) { + return false; + } + + for (HabboItem item : matchTargets) { + if (this.matchesType(item, compareTypeIds)) { + return true; + } + } + + return false; + } + + protected List resolveMatchTargets(WiredContext ctx) { + this.refresh(); + return this.resolveConfiguredItems(ctx, this.furniSource); + } + + protected THashSet resolveCompareTypeIds(WiredContext ctx) { this.refresh(); - if(items.isEmpty()) - return false; + THashSet compareTypeIds = new THashSet<>(); - List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); - if (targets.isEmpty()) return false; - return targets.stream().anyMatch(this.items::contains); + for (HabboItem item : this.resolveConfiguredItems(ctx, this.compareFurniSource)) { + if (item != null && item.getBaseItem() != null) { + compareTypeIds.add(item.getBaseItem().getId()); + } + } + + return compareTypeIds; + } + + protected boolean matchesType(HabboItem item, THashSet compareTypeIds) { + return item != null && item.getBaseItem() != null && compareTypeIds.contains(item.getBaseItem().getId()); + } + + protected List resolveConfiguredItems(WiredContext ctx, int sourceType) { + if (sourceType == SOURCE_SECONDARY_SELECTED) { + return new ArrayList<>(this.secondaryItems); + } + + return WiredSourceUtil.resolveItems(ctx, sourceType, this.items); } @Deprecated @@ -62,41 +141,53 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition { this.refresh(); return WiredManager.getGson().toJson(new JsonData( this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), - this.furniSource + this.secondaryItems.stream().map(HabboItem::getId).collect(Collectors.toList()), + this.furniSource, + this.compareFurniSource, + this.quantifier )); } @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { - this.items.clear(); + this.onPickUp(); + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.furniSource = data.furniSource; - - for(int id : data.itemIds) { - HabboItem item = room.getHabboItem(id); - - if (item != null) { - this.items.add(item); - } + if (data == null) { + return; } - } else { - String[] data = wiredData.split(";"); - for (String s : data) { - HabboItem item = room.getHabboItem(Integer.parseInt(s)); + List primaryIds = (data.primaryItemIds != null) ? data.primaryItemIds : new ArrayList<>(); + List compareIds = (data.secondaryItemIds != null) ? data.secondaryItemIds : ((data.itemIds != null) ? data.itemIds : new ArrayList<>()); - if (item != null) { - this.items.add(item); - } + this.furniSource = this.normalizeFurniSource((data.furniSource != null) ? data.furniSource : WiredSourceUtil.SOURCE_TRIGGER); + this.compareFurniSource = this.normalizeFurniSource((data.compareFurniSource != null) ? data.compareFurniSource : (compareIds.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : SOURCE_SECONDARY_SELECTED)); + this.quantifier = this.normalizeQuantifier((data.quantifier != null) ? data.quantifier : QUANTIFIER_ANY); + + this.loadItems(room, primaryIds, this.items); + this.loadItems(room, compareIds, this.secondaryItems); + return; + } + + String[] data = wiredData.split(";"); + List compareIds = new ArrayList<>(); + + for (String value : data) { + try { + compareIds.add(Integer.parseInt(value)); + } catch (NumberFormatException ignored) { } - this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; - } - if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; } + + this.loadItems(room, compareIds, this.secondaryItems); + this.compareFurniSource = this.secondaryItems.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : SOURCE_SECONDARY_SELECTED; + this.quantifier = QUANTIFIER_ANY; } @Override @@ -112,14 +203,17 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition { message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); message.appendInt(this.items.size()); - for (HabboItem item : this.items) + for (HabboItem item : this.items) { message.appendInt(item.getId()); + } message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); - message.appendString(""); - message.appendInt(1); + message.appendString(this.serializeIds(this.secondaryItems)); + message.appendInt(3); message.appendInt(this.furniSource); + message.appendInt(this.compareFurniSource); + message.appendInt(this.quantifier); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -132,16 +226,55 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition { if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; int[] params = settings.getIntParams(); - this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + String stringParam = (settings.getStringParam() != null) ? settings.getStringParam().trim() : ""; + boolean legacyData = (params.length <= 1) && stringParam.isEmpty(); - this.items.clear(); + this.onPickUp(); + + if (legacyData) { + this.furniSource = (params.length > 0) ? this.normalizeFurniSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ANY; + } else { + this.furniSource = (params.length > 0) ? this.normalizeFurniSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER; + this.compareFurniSource = (params.length > 1) ? this.normalizeFurniSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL; + } + + if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + return false; + } + + if (legacyData) { + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + if (item != null) { + this.secondaryItems.add(item); + } + } + + this.compareFurniSource = this.secondaryItems.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : SOURCE_SECONDARY_SELECTED; + return true; + } if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { - Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + if (item != null) { + this.items.add(item); + } + } + } - if (room != null) { - for (int i = 0; i < count; i++) { - this.items.add(room.getHabboItem(settings.getFurniIds()[i])); + if (this.compareFurniSource == SOURCE_SECONDARY_SELECTED) { + for (Integer itemId : this.parseIds(stringParam)) { + HabboItem item = room.getHabboItem(itemId); + if (item != null) { + this.secondaryItems.add(item); } } } @@ -149,31 +282,110 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition { return true; } - private void refresh() { - THashSet items = new THashSet<>(); + protected int getQuantifier() { + return this.quantifier; + } + + protected void refresh() { + this.refreshSelection(this.items); + this.refreshSelection(this.secondaryItems); + } + + private void refreshSelection(THashSet selection) { + THashSet remove = new THashSet<>(); Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); if (room == null) { - items.addAll(this.items); + remove.addAll(selection); } else { - for (HabboItem item : this.items) { - if (room.getHabboItem(item.getId()) == null) - items.add(item); + for (HabboItem item : selection) { + if (room.getHabboItem(item.getId()) == null) { + remove.add(item); + } } } - for (HabboItem item : items) { - this.items.remove(item); + for (HabboItem item : remove) { + selection.remove(item); } } + private void loadItems(Room room, List itemIds, THashSet target) { + if (itemIds == null) { + return; + } + + for (Integer id : itemIds) { + if (id == null) { + continue; + } + + HabboItem item = room.getHabboItem(id); + if (item != null) { + target.add(item); + } + } + } + + private String serializeIds(THashSet source) { + return source.stream() + .map(HabboItem::getId) + .filter(id -> id > 0) + .map(String::valueOf) + .collect(Collectors.joining(";")); + } + + private List parseIds(String value) { + List result = new ArrayList<>(); + if (value == null || value.isEmpty()) { + return result; + } + + for (String part : value.split("[;,\\t]")) { + if (part == null || part.trim().isEmpty()) { + continue; + } + + try { + result.add(Integer.parseInt(part.trim())); + } catch (NumberFormatException ignored) { + } + } + + return result; + } + + protected int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_TRIGGER: + case WiredSourceUtil.SOURCE_SELECTED: + case SOURCE_SECONDARY_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + protected int normalizeQuantifier(int value) { + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + static class JsonData { + List primaryItemIds; + List secondaryItemIds; List itemIds; - int furniSource; + Integer furniSource; + Integer compareFurniSource; + Integer quantifier; - public JsonData(List itemIds, int furniSource) { - this.itemIds = itemIds; + public JsonData(List primaryItemIds, List secondaryItemIds, int furniSource, int compareFurniSource, int quantifier) { + this.primaryItemIds = primaryItemIds; + this.secondaryItemIds = secondaryItemIds; this.furniSource = furniSource; + this.compareFurniSource = compareFurniSource; + this.quantifier = quantifier; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionGroupMember.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionGroupMember.java index aa9631b5..441a5989 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionGroupMember.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionGroupMember.java @@ -8,6 +8,7 @@ import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.wired.WiredConditionType; import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; @@ -16,8 +17,16 @@ import java.sql.SQLException; import java.util.List; public class WiredConditionGroupMember extends InteractionWiredCondition { + private static final int GROUP_CURRENT_ROOM = 0; + private static final int GROUP_SELECTED = 1; + protected static final int QUANTIFIER_ALL = 0; + protected static final int QUANTIFIER_ANY = 1; + public static final WiredConditionType type = WiredConditionType.ACTOR_IN_GROUP; private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int groupType = GROUP_CURRENT_ROOM; + private int selectedGroupId = 0; + private int quantifier = QUANTIFIER_ALL; public WiredConditionGroupMember(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -30,19 +39,32 @@ public class WiredConditionGroupMember extends InteractionWiredCondition { @Override public boolean evaluate(WiredContext ctx) { Room room = ctx.room(); - if (room.getGuildId() == 0) + int targetGroupId = this.resolveTargetGroupId(room); + if (targetGroupId == 0) return false; List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); if (targets.isEmpty()) return false; + if (this.quantifier == QUANTIFIER_ANY) { + for (RoomUnit roomUnit : targets) { + Habbo habbo = room.getHabbo(roomUnit); + if (habbo != null && habbo.getHabboStats().hasGuild(targetGroupId)) { + return true; + } + } + + return false; + } + for (RoomUnit roomUnit : targets) { Habbo habbo = room.getHabbo(roomUnit); - if (habbo != null && habbo.getHabboStats().hasGuild(room.getGuildId())) { - return true; + if (habbo == null || !habbo.getHabboStats().hasGuild(targetGroupId)) { + return false; } } - return false; + + return true; } @Deprecated @@ -53,26 +75,45 @@ public class WiredConditionGroupMember extends InteractionWiredCondition { @Override public String getWiredData() { - return String.valueOf(this.userSource); + return WiredManager.getGson().toJson(new JsonData( + this.userSource, + this.groupType, + this.selectedGroupId, + this.quantifier + )); } @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.resetSettings(); + String wiredData = set.getString("wired_data"); - if (wiredData != null && !wiredData.isEmpty()) { - try { - this.userSource = Integer.parseInt(wiredData); - } catch (NumberFormatException ignored) { - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + try { + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.userSource = this.normalizeUserSource(data.userSource); + this.groupType = this.normalizeGroupType(data.groupType); + this.selectedGroupId = this.normalizeSelectedGroupId(data.selectedGroupId); + this.quantifier = this.normalizeQuantifier(data.quantifier); + return; } - } else { - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.userSource = this.normalizeUserSource(Integer.parseInt(wiredData)); + } catch (Exception ignored) { + this.resetSettings(); } } @Override public void onPickUp() { - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.resetSettings(); } @Override @@ -88,8 +129,11 @@ public class WiredConditionGroupMember extends InteractionWiredCondition { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(1); + message.appendInt(4); message.appendInt(this.userSource); + message.appendInt(this.groupType); + message.appendInt(this.selectedGroupId); + message.appendInt(this.quantifier); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -99,7 +143,63 @@ public class WiredConditionGroupMember extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { int[] params = settings.getIntParams(); - this.userSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.userSource = (params.length > 0) ? this.normalizeUserSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER; + this.groupType = (params.length > 1) ? this.normalizeGroupType(params[1]) : GROUP_CURRENT_ROOM; + this.selectedGroupId = (params.length > 2) ? this.normalizeSelectedGroupId(params[2]) : 0; + this.quantifier = (params.length > 3) ? this.normalizeQuantifier(params[3]) : QUANTIFIER_ALL; return true; } + + private void resetSettings() { + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.groupType = GROUP_CURRENT_ROOM; + this.selectedGroupId = 0; + this.quantifier = QUANTIFIER_ALL; + } + + private int resolveTargetGroupId(Room room) { + if (room == null) { + return 0; + } + + if (this.groupType == GROUP_SELECTED) { + return this.selectedGroupId; + } + + return room.getGuildId(); + } + + private int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private int normalizeGroupType(int value) { + return (value == GROUP_SELECTED) ? GROUP_SELECTED : GROUP_CURRENT_ROOM; + } + + private int normalizeSelectedGroupId(int value) { + return Math.max(0, value); + } + + protected int getQuantifier() { + return this.quantifier; + } + + protected int normalizeQuantifier(int value) { + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + + static class JsonData { + int userSource; + int groupType; + int selectedGroupId; + int quantifier; + + public JsonData(int userSource, int groupType, int selectedGroupId, int quantifier) { + this.userSource = userSource; + this.groupType = groupType; + this.selectedGroupId = selectedGroupId; + this.quantifier = quantifier; + } + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasEffect.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasEffect.java index a184beaf..2075513d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasEffect.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasEffect.java @@ -16,10 +16,14 @@ import java.sql.SQLException; import java.util.List; public class WiredConditionHabboHasEffect extends InteractionWiredCondition { + protected static final int QUANTIFIER_ALL = 0; + protected static final int QUANTIFIER_ANY = 1; + public static final WiredConditionType type = WiredConditionType.ACTOR_WEARS_EFFECT; protected int effectId = 0; - private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int userSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int quantifier = QUANTIFIER_ANY; public WiredConditionHabboHasEffect(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -33,14 +37,38 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition { public boolean evaluate(WiredContext ctx) { List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); if (targets.isEmpty()) return false; + + if (this.quantifier == QUANTIFIER_ALL) { + return this.matchesAllTargets(targets); + } + + return this.matchesAnyTarget(targets); + } + + protected boolean matchesAllTargets(List targets) { for (RoomUnit roomUnit : targets) { - if (roomUnit != null && roomUnit.getEffectId() == this.effectId) { + if (!this.matchesEffect(roomUnit)) { + return false; + } + } + + return true; + } + + protected boolean matchesAnyTarget(List targets) { + for (RoomUnit roomUnit : targets) { + if (this.matchesEffect(roomUnit)) { return true; } } + return false; } + protected boolean matchesEffect(RoomUnit roomUnit) { + return roomUnit != null && roomUnit.getEffectId() == this.effectId; + } + @Deprecated @Override public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { @@ -51,7 +79,8 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition { public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( this.effectId, - this.userSource + this.userSource, + this.quantifier )); } @@ -63,9 +92,11 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.effectId = data.effectId; this.userSource = data.userSource; + this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY); } else { this.effectId = Integer.parseInt(wiredData); this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ANY; } } @@ -73,6 +104,7 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition { public void onPickUp() { this.effectId = 0; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ANY; } @Override @@ -88,9 +120,10 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(2); + message.appendInt(3); message.appendInt(this.effectId); message.appendInt(this.userSource); + message.appendInt(this.quantifier); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -100,20 +133,35 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 1) return false; - this.effectId = settings.getIntParams()[0]; int[] params = settings.getIntParams(); + this.effectId = params[0]; this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2], QUANTIFIER_ANY) : QUANTIFIER_ANY; return true; } + protected int getQuantifier() { + return this.quantifier; + } + + protected int normalizeQuantifier(Integer value, int fallback) { + if (value == null) { + return fallback; + } + + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + static class JsonData { int effectId; int userSource; + Integer quantifier; - public JsonData(int effectId, int userSource) { + public JsonData(int effectId, int userSource, int quantifier) { this.effectId = effectId; this.userSource = userSource; + this.quantifier = quantifier; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasHandItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasHandItem.java index 1a064793..d949e4bd 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasHandItem.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasHandItem.java @@ -19,11 +19,14 @@ import java.util.List; public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { private static final Logger LOGGER = LoggerFactory.getLogger(WiredConditionHabboHasHandItem.class); + protected static final int QUANTIFIER_ALL = 0; + protected static final int QUANTIFIER_ANY = 1; public static final WiredConditionType type = WiredConditionType.ACTOR_HAS_HANDITEM; private int handItem; private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int quantifier = QUANTIFIER_ALL; public WiredConditionHabboHasHandItem(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -46,9 +49,10 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(2); + message.appendInt(3); message.appendInt(this.handItem); message.appendInt(this.userSource); + message.appendInt(this.quantifier); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -58,9 +62,10 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 1) return false; - this.handItem = settings.getIntParams()[0]; + this.handItem = this.normalizeHandItem(settings.getIntParams()[0]); int[] params = settings.getIntParams(); this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL; return true; } @@ -69,12 +74,12 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { public boolean evaluate(WiredContext ctx) { List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); if (targets.isEmpty()) return false; - for (RoomUnit roomUnit : targets) { - if (roomUnit != null && roomUnit.getHandItem() == this.handItem) { - return true; - } + + if (this.quantifier == QUANTIFIER_ANY) { + return this.matchesAnyTarget(targets); } - return false; + + return this.matchesAllTargets(targets); } @Deprecated @@ -87,7 +92,8 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( this.handItem, - this.userSource + this.userSource, + this.quantifier )); } @@ -98,11 +104,13 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.handItem = data.handItemId; + this.handItem = this.normalizeHandItem(data.handItemId); this.userSource = data.userSource; + this.quantifier = this.normalizeQuantifier(data.quantifier); } else { - this.handItem = Integer.parseInt(wiredData); + this.handItem = this.normalizeHandItem(Integer.parseInt(wiredData)); this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; } } catch (Exception e) { LOGGER.error("Caught exception", e); @@ -113,15 +121,58 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { public void onPickUp() { this.handItem = 0; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; + } + + protected int getHandItem() { + return this.handItem; + } + + protected int getUserSource() { + return this.userSource; + } + + protected int getQuantifier() { + return this.quantifier; + } + + protected boolean matchesAnyTarget(List targets) { + for (RoomUnit roomUnit : targets) { + if (roomUnit != null && roomUnit.getHandItem() == this.handItem) { + return true; + } + } + + return false; + } + + protected boolean matchesAllTargets(List targets) { + for (RoomUnit roomUnit : targets) { + if (roomUnit == null || roomUnit.getHandItem() != this.handItem) { + return false; + } + } + + return true; + } + + protected int normalizeHandItem(int value) { + return Math.max(0, value); + } + + protected int normalizeQuantifier(int value) { + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; } static class JsonData { int handItemId; int userSource; + int quantifier; - public JsonData(int handItemId, int userSource) { + public JsonData(int handItemId, int userSource, int quantifier) { this.handItemId = handItemId; this.userSource = userSource; + this.quantifier = quantifier; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboWearsBadge.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboWearsBadge.java index 844ef89c..83c85a8f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboWearsBadge.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboWearsBadge.java @@ -18,10 +18,14 @@ import java.sql.SQLException; import java.util.List; public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { + protected static final int QUANTIFIER_ALL = 0; + protected static final int QUANTIFIER_ANY = 1; + public static final WiredConditionType type = WiredConditionType.ACTOR_WEARS_BADGE; protected String badge = ""; - private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int userSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int quantifier = QUANTIFIER_ANY; public WiredConditionHabboWearsBadge(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -37,18 +41,47 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); if (targets.isEmpty()) return false; + if (this.quantifier == QUANTIFIER_ALL) { + return this.matchesAllTargets(room, targets); + } + + return this.matchesAnyTarget(room, targets); + } + + protected boolean matchesAllTargets(Room room, List targets) { for (RoomUnit roomUnit : targets) { - Habbo habbo = room.getHabbo(roomUnit); - if (habbo != null) { - synchronized (habbo.getInventory().getBadgesComponent().getWearingBadges()) { - for (HabboBadge badge : habbo.getInventory().getBadgesComponent().getWearingBadges()) { - if (badge.getCode().equalsIgnoreCase(this.badge)) { - return true; - } - } + if (!this.matchesBadge(room, roomUnit)) { + return false; + } + } + + return true; + } + + protected boolean matchesAnyTarget(Room room, List targets) { + for (RoomUnit roomUnit : targets) { + if (this.matchesBadge(room, roomUnit)) { + return true; + } + } + + return false; + } + + protected boolean matchesBadge(Room room, RoomUnit roomUnit) { + Habbo habbo = room.getHabbo(roomUnit); + if (habbo == null) { + return false; + } + + synchronized (habbo.getInventory().getBadgesComponent().getWearingBadges()) { + for (HabboBadge badge : habbo.getInventory().getBadgesComponent().getWearingBadges()) { + if (badge.getCode().equalsIgnoreCase(this.badge)) { + return true; } } } + return false; } @@ -62,7 +95,8 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( this.badge, - this.userSource + this.userSource, + this.quantifier )); } @@ -74,9 +108,11 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.badge = data.badge; this.userSource = data.userSource; + this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY); } else { this.badge = wiredData; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ANY; } } @@ -84,6 +120,7 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { public void onPickUp() { this.badge = ""; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ANY; } @Override @@ -99,8 +136,9 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(this.badge); - message.appendInt(1); + message.appendInt(2); message.appendInt(this.userSource); + message.appendInt(this.quantifier); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -112,17 +150,32 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { this.badge = settings.getStringParam(); int[] params = settings.getIntParams(); this.userSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = (params.length > 1) ? this.normalizeQuantifier(params[1], QUANTIFIER_ANY) : QUANTIFIER_ANY; return true; } + protected int getQuantifier() { + return this.quantifier; + } + + protected int normalizeQuantifier(Integer value, int fallback) { + if (value == null) { + return fallback; + } + + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + static class JsonData { String badge; int userSource; + Integer quantifier; - public JsonData(String badge, int userSource) { + public JsonData(String badge, int userSource, int quantifier) { this.badge = badge; this.userSource = userSource; + this.quantifier = quantifier; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHasAltitude.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHasAltitude.java new file mode 100644 index 00000000..43a71b8f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHasAltitude.java @@ -0,0 +1,289 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import gnu.trove.set.hash.THashSet; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.stream.Collectors; + +public class WiredConditionHasAltitude extends InteractionWiredCondition { + private static final int COMPARISON_LESS = 0; + private static final int COMPARISON_EQUAL = 1; + private static final int COMPARISON_GREATER = 2; + private static final int QUANTIFIER_ALL = 0; + private static final int QUANTIFIER_ANY = 1; + + public static final WiredConditionType type = WiredConditionType.HAS_ALTITUDE; + + private final THashSet items; + private int comparison = COMPARISON_EQUAL; + private double altitude = 0.0D; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private int quantifier = QUANTIFIER_ALL; + + public WiredConditionHasAltitude(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + this.items = new THashSet<>(); + } + + public WiredConditionHasAltitude(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + this.items = new THashSet<>(); + } + + @Override + public boolean evaluate(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) { + return false; + } + + this.refresh(room); + + List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + if (targets.isEmpty()) { + return false; + } + + if (this.quantifier == QUANTIFIER_ANY) { + return targets.stream().anyMatch(this::matchesAltitude); + } + + return targets.stream().allMatch(this::matchesAltitude); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.comparison, + this.formatAltitude(this.altitude), + this.furniSource, + this.quantifier, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.items.clear(); + this.comparison = COMPARISON_EQUAL; + this.altitude = 0.0D; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.comparison = this.normalizeComparison(data.comparison); + this.altitude = this.parseAltitudeOrDefault(data.altitude); + this.furniSource = this.normalizeFurniSource(data.furniSource); + this.quantifier = this.normalizeQuantifier(data.quantifier); + + if (data.itemIds == null) { + return; + } + + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + if (item != null) { + this.items.add(item); + } + } + } + + @Override + public void onPickUp() { + this.items.clear(); + this.comparison = COMPARISON_EQUAL; + this.altitude = 0.0D; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(room); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(this.items.size()); + + for (HabboItem item : this.items) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.formatAltitude(this.altitude)); + message.appendInt(3); + message.appendInt(this.comparison); + message.appendInt(this.furniSource); + message.appendInt(this.quantifier); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] params = settings.getIntParams(); + this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL; + this.furniSource = (params.length > 1) ? this.normalizeFurniSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL; + this.altitude = this.parseAltitudeOrDefault(settings.getStringParam()); + + int count = settings.getFurniIds().length; + if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + return false; + } + + if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + + this.items.clear(); + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + return false; + } + + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + if (item != null) { + this.items.add(item); + } + } + } + + return true; + } + + private boolean matchesAltitude(HabboItem item) { + if (item == null) { + return false; + } + + double normalizedAltitude = this.normalizeAltitude(item.getZ()); + + switch (this.comparison) { + case COMPARISON_LESS: + return normalizedAltitude < this.altitude; + case COMPARISON_GREATER: + return normalizedAltitude > this.altitude; + default: + return BigDecimal.valueOf(normalizedAltitude).compareTo(BigDecimal.valueOf(this.altitude)) == 0; + } + } + + private void refresh(Room room) { + THashSet remove = new THashSet<>(); + + for (HabboItem item : this.items) { + if (room.getHabboItem(item.getId()) == null) { + remove.add(item); + } + } + + for (HabboItem item : remove) { + this.items.remove(item); + } + } + + private int normalizeComparison(int value) { + if (value < COMPARISON_LESS || value > COMPARISON_GREATER) { + return COMPARISON_EQUAL; + } + + return value; + } + + private int normalizeQuantifier(int value) { + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + + private int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_TRIGGER: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + private double normalizeAltitude(double value) { + double clampedValue = Math.max(0.0D, Math.min(Room.MAXIMUM_FURNI_HEIGHT, value)); + return BigDecimal.valueOf(clampedValue).setScale(2, RoundingMode.HALF_UP).doubleValue(); + } + + private double parseAltitudeOrDefault(String value) { + if (value == null || value.trim().isEmpty()) { + return 0.0D; + } + + try { + return this.normalizeAltitude(new BigDecimal(value.trim()).doubleValue()); + } catch (NumberFormatException exception) { + return 0.0D; + } + } + + private String formatAltitude(double value) { + BigDecimal decimal = BigDecimal.valueOf(this.normalizeAltitude(value)).stripTrailingZeros(); + return (decimal.scale() < 0 ? decimal.setScale(0, RoundingMode.DOWN) : decimal).toPlainString(); + } + + static class JsonData { + int comparison; + String altitude; + int furniSource; + int quantifier; + List itemIds; + + public JsonData(int comparison, String altitude, int furniSource, int quantifier, List itemIds) { + this.comparison = comparison; + this.altitude = altitude; + this.furniSource = furniSource; + this.quantifier = quantifier; + this.itemIds = itemIds; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHasVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHasVariable.java new file mode 100644 index 00000000..65349024 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHasVariable.java @@ -0,0 +1,506 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomRightLevels; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.users.DanceType; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; +import com.eu.habbo.habbohotel.wired.core.WiredInternalVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import gnu.trove.set.hash.THashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredConditionHasVariable extends InteractionWiredCondition { + private static final Logger LOGGER = LoggerFactory.getLogger(WiredConditionHasVariable.class); + + protected static final int TARGET_USER = 0; + protected static final int TARGET_FURNI = 1; + protected static final int TARGET_CONTEXT = 2; + protected static final int TARGET_ROOM = 3; + protected static final int QUANTIFIER_ALL = 0; + protected static final int QUANTIFIER_ANY = 1; + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + private static final String INTERNAL_TOKEN_PREFIX = "internal:"; + + public static final WiredConditionType type = WiredConditionType.HAS_VAR; + + protected final THashSet selectedItems = new THashSet<>(); + protected int targetType = TARGET_USER; + protected int userSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int quantifier = QUANTIFIER_ALL; + protected String variableToken = ""; + protected int variableItemId = 0; + + public WiredConditionHasVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionHasVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(); + + List serializedItems = new ArrayList<>(); + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + serializedItems.addAll(this.selectedItems); + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(serializedItems.size()); + + for (HabboItem item : serializedItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableToken == null ? "" : this.variableToken); + message.appendInt(4); + message.appendInt(this.targetType); + message.appendInt(this.userSource); + message.appendInt(this.furniSource); + message.appendInt(this.quantifier); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] params = settings.getIntParams(); + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + this.targetType = (params.length > 0) ? normalizeTargetType(params[0]) : TARGET_USER; + this.userSource = (params.length > 1) ? normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = (params.length > 2) ? normalizeFurniSource(params[2]) : WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = (params.length > 3) ? normalizeQuantifier(params[3]) : QUANTIFIER_ALL; + this.setVariableToken(normalizeVariableToken(settings.getStringParam())); + + if (this.variableToken.isEmpty()) { + return false; + } + + this.selectedItems.clear(); + + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED && room != null) { + int[] furniIds = settings.getFurniIds(); + if (furniIds.length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + return false; + } + + for (int furniId : furniIds) { + HabboItem item = room.getHabboItem(furniId); + + if (item != null) { + this.selectedItems.add(item); + } + } + } + + return true; + } + + @Override + public boolean evaluate(WiredContext ctx) { + return this.evaluateWithNegation(ctx, false); + } + + protected boolean evaluateWithNegation(WiredContext ctx, boolean negative) { + Room room = ctx.room(); + + if (room == null || this.variableToken == null || this.variableToken.isEmpty()) { + return false; + } + + return switch (this.targetType) { + case TARGET_FURNI -> this.evaluateFurniTargets(ctx, room, negative); + case TARGET_CONTEXT -> { + boolean contextMatch = this.matchesContext(ctx, room); + yield negative ? !contextMatch : contextMatch; + } + case TARGET_ROOM -> { + boolean roomMatch = this.matchesRoom(room); + yield negative ? !roomMatch : roomMatch; + } + default -> this.evaluateUserTargets(ctx, room, negative); + }; + } + + private boolean evaluateUserTargets(WiredContext ctx, Room room, boolean negative) { + List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); + if (targets.isEmpty()) return false; + + boolean match = (this.quantifier == QUANTIFIER_ANY) + ? this.matchesAnyUser(room, targets) + : this.matchesAllUsers(room, targets); + + return negative ? !match : match; + } + + private boolean evaluateFurniTargets(WiredContext ctx, Room room, boolean negative) { + this.refresh(); + + List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.selectedItems); + if (targets.isEmpty()) return false; + + boolean match = (this.quantifier == QUANTIFIER_ANY) + ? this.matchesAnyFurni(room, targets) + : this.matchesAllFurni(room, targets); + + return negative ? !match : match; + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + this.refresh(); + + List itemIds = new ArrayList<>(); + for (HabboItem item : this.selectedItems) { + if (item != null) itemIds.add(item.getId()); + } + + return WiredManager.getGson().toJson(new JsonData( + itemIds, + this.targetType, + this.variableToken, + this.variableItemId, + this.userSource, + this.furniSource, + this.quantifier + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) return; + + try { + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data == null) return; + + this.targetType = normalizeTargetType(data.targetType); + this.userSource = normalizeUserSource(data.userSource); + this.furniSource = normalizeFurniSource(data.furniSource); + this.quantifier = normalizeQuantifier(data.quantifier); + this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); + + if (room != null && data.itemIds != null) { + for (Integer itemId : data.itemIds) { + if (itemId == null || itemId <= 0) continue; + + HabboItem item = room.getHabboItem(itemId); + if (item != null) this.selectedItems.add(item); + } + } + + return; + } + + this.setVariableToken(normalizeVariableToken(wiredData)); + } catch (Exception e) { + LOGGER.error("Failed to load wired variable condition data for item {}", this.getId(), e); + this.onPickUp(); + } + } + + @Override + public void onPickUp() { + this.targetType = TARGET_USER; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; + this.selectedItems.clear(); + this.setVariableToken(""); + } + + protected boolean matchesAnyUser(Room room, List targets) { + for (RoomUnit roomUnit : targets) { + if (this.matchesUser(room, roomUnit)) { + return true; + } + } + + return false; + } + + protected boolean matchesAllUsers(Room room, List targets) { + for (RoomUnit roomUnit : targets) { + if (!this.matchesUser(room, roomUnit)) { + return false; + } + } + + return true; + } + + protected boolean matchesAnyFurni(Room room, List targets) { + for (HabboItem item : targets) { + if (this.matchesFurni(room, item)) { + return true; + } + } + + return false; + } + + protected boolean matchesAllFurni(Room room, List targets) { + for (HabboItem item : targets) { + if (!this.matchesFurni(room, item)) { + return false; + } + } + + return true; + } + + protected boolean matchesUser(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) { + return false; + } + + if (isCustomVariableToken(this.variableToken)) { + Habbo habbo = room.getHabbo(roomUnit); + + return habbo != null && room.getUserVariableManager().hasVariable(habbo.getHabboInfo().getId(), this.variableItemId); + } + + if (isInternalVariableToken(this.variableToken)) { + return this.hasUserInternalVariable(room, roomUnit, getInternalVariableKey(this.variableToken)); + } + + return false; + } + + protected boolean matchesFurni(Room room, HabboItem item) { + if (room == null || item == null) { + return false; + } + + if (isCustomVariableToken(this.variableToken)) { + return room.getFurniVariableManager().hasVariable(item.getId(), this.variableItemId); + } + + if (isInternalVariableToken(this.variableToken)) { + return this.hasFurniInternalVariable(item, getInternalVariableKey(this.variableToken)); + } + + return false; + } + + protected boolean matchesContext(WiredContext ctx, Room room) { + if (ctx == null || room == null) { + return false; + } + + if (isCustomVariableToken(this.variableToken)) { + return WiredContextVariableSupport.hasVariable(ctx, this.variableItemId); + } + + if (isInternalVariableToken(this.variableToken)) { + return WiredInternalVariableSupport.readContextValue(ctx, getInternalVariableKey(this.variableToken)) != null; + } + + return false; + } + + protected boolean matchesRoom(Room room) { + if (room == null) { + return false; + } + + if (isCustomVariableToken(this.variableToken)) { + return room.getRoomVariableManager().hasVariable(this.variableItemId); + } + + if (isInternalVariableToken(this.variableToken)) { + return this.hasRoomInternalVariable(getInternalVariableKey(this.variableToken)); + } + + return false; + } + + protected boolean hasUserInternalVariable(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.hasUserValue(room, roomUnit, key); + } + + protected boolean hasFurniInternalVariable(HabboItem item, String key) { + return WiredInternalVariableSupport.hasFurniValue(item, key); + } + + protected boolean hasRoomInternalVariable(String key) { + return WiredInternalVariableSupport.hasRoomValue(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()), key); + } + + protected void refresh() { + THashSet staleItems = new THashSet<>(); + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + staleItems.addAll(this.selectedItems); + } else { + for (HabboItem item : this.selectedItems) { + if (item == null || item.getRoomId() != room.getId()) { + staleItems.add(item); + } + } + } + + this.selectedItems.removeAll(staleItems); + } + + protected void setVariableToken(String token) { + this.variableToken = normalizeVariableToken(token); + this.variableItemId = getCustomItemId(this.variableToken); + } + + protected boolean hasRoomEntryMethod(Habbo habbo) { + if (habbo == null) return false; + + String roomEntryMethod = habbo.getHabboInfo().getRoomEntryMethod(); + + return roomEntryMethod != null && !roomEntryMethod.trim().isEmpty() && !"unknown".equalsIgnoreCase(roomEntryMethod); + } + + protected TeamEffectData getTeamEffectData(int effectValue) { + if (effectValue <= 0) return null; + + if (effectValue >= 223 && effectValue <= 226) return new TeamEffectData(effectValue - 222, 0); + if (effectValue >= 33 && effectValue <= 36) return new TeamEffectData(effectValue - 32, 1); + if (effectValue >= 40 && effectValue <= 43) return new TeamEffectData(effectValue - 39, 2); + + return null; + } + + protected static int normalizeTargetType(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + protected static int normalizeQuantifier(int value) { + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + + protected static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + protected static int normalizeFurniSource(int value) { + return switch (value) { + case WiredSourceUtil.SOURCE_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + } + + protected static boolean isCustomVariableToken(String token) { + return token != null && token.startsWith(CUSTOM_TOKEN_PREFIX); + } + + protected static boolean isInternalVariableToken(String token) { + return token != null && token.startsWith(INTERNAL_TOKEN_PREFIX); + } + + protected static int getCustomItemId(String token) { + if (!isCustomVariableToken(token)) return 0; + + try { + return Integer.parseInt(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } catch (NumberFormatException e) { + return 0; + } + } + + protected static String getInternalVariableKey(String token) { + return isInternalVariableToken(token) ? WiredInternalVariableSupport.normalizeKey(token.substring(INTERNAL_TOKEN_PREFIX.length())) : ""; + } + + protected static String normalizeVariableToken(String token) { + if (token == null) return ""; + + String normalized = token.trim(); + if (normalized.isEmpty()) return ""; + if (isCustomVariableToken(normalized)) return normalized; + if (isInternalVariableToken(normalized)) return INTERNAL_TOKEN_PREFIX + WiredInternalVariableSupport.normalizeKey(normalized.substring(INTERNAL_TOKEN_PREFIX.length())); + + try { + int parsed = Integer.parseInt(normalized); + return (parsed > 0) ? (CUSTOM_TOKEN_PREFIX + parsed) : ""; + } catch (NumberFormatException e) { + return ""; + } + } + + protected static class JsonData { + List itemIds; + int targetType; + String variableToken; + int variableItemId; + int userSource; + int furniSource; + int quantifier; + + public JsonData(List itemIds, int targetType, String variableToken, int variableItemId, int userSource, int furniSource, int quantifier) { + this.itemIds = itemIds; + this.targetType = targetType; + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.userSource = userSource; + this.furniSource = furniSource; + this.quantifier = quantifier; + } + } + + protected static class TeamEffectData { + final int colorId; + final int typeId; + + protected TeamEffectData(int colorId, int typeId) { + this.colorId = colorId; + this.typeId = typeId; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchDate.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchDate.java new file mode 100644 index 00000000..3574480b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchDate.java @@ -0,0 +1,253 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.util.HotelDateTimeUtil; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; + +public class WiredConditionMatchDate extends InteractionWiredCondition { + private static final int MODE_SKIP = 0; + private static final int MODE_EXACT = 1; + private static final int MODE_RANGE = 2; + private static final int ALL_WEEKDAYS_MASK = createMask(1, 7); + private static final int ALL_MONTHS_MASK = createMask(1, 12); + + public static final WiredConditionType type = WiredConditionType.MATCH_DATE; + + private int weekdayMask = ALL_WEEKDAYS_MASK; + private int dayMode = MODE_SKIP; + private int dayFrom = 1; + private int dayTo = 31; + private int monthMask = ALL_MONTHS_MASK; + private int yearMode = MODE_SKIP; + private int yearFrom = HotelDateTimeUtil.localDateNow().getYear(); + private int yearTo = HotelDateTimeUtil.localDateNow().getYear(); + + public WiredConditionMatchDate(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionMatchDate(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(8); + message.appendInt(this.weekdayMask); + message.appendInt(this.dayMode); + message.appendInt(this.dayFrom); + message.appendInt(this.dayTo); + message.appendInt(this.monthMask); + message.appendInt(this.yearMode); + message.appendInt(this.yearFrom); + message.appendInt(this.yearTo); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] params = settings.getIntParams(); + + this.weekdayMask = (params.length > 0) ? this.normalizeWeekdayMask(params[0]) : ALL_WEEKDAYS_MASK; + this.dayMode = (params.length > 1) ? this.normalizeMode(params[1]) : MODE_SKIP; + this.dayFrom = (params.length > 2) ? this.normalizeDay(params[2]) : 1; + this.dayTo = (params.length > 3) ? this.normalizeDay(params[3]) : this.dayFrom; + this.monthMask = (params.length > 4) ? this.normalizeMonthMask(params[4]) : ALL_MONTHS_MASK; + this.yearMode = (params.length > 5) ? this.normalizeMode(params[5]) : MODE_SKIP; + this.yearFrom = (params.length > 6) ? this.normalizeYear(params[6]) : HotelDateTimeUtil.localDateNow().getYear(); + this.yearTo = (params.length > 7) ? this.normalizeYear(params[7]) : this.yearFrom; + + return true; + } + + @Override + public boolean evaluate(WiredContext ctx) { + LocalDate now = HotelDateTimeUtil.localDateNow(); + + return this.matchesMask(now.getDayOfWeek().getValue(), this.weekdayMask) + && this.matchesMask(now.getMonthValue(), this.monthMask) + && this.matchesDatePart(now.getDayOfMonth(), this.dayMode, this.dayFrom, this.dayTo) + && this.matchesDatePart(now.getYear(), this.yearMode, this.yearFrom, this.yearTo); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.weekdayMask, + this.dayMode, + this.dayFrom, + this.dayTo, + this.monthMask, + this.yearMode, + this.yearFrom, + this.yearTo + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.reset(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.weekdayMask = this.normalizeWeekdayMask(data.weekdayMask); + this.dayMode = this.normalizeMode(data.dayMode); + this.dayFrom = this.normalizeDay(data.dayFrom); + this.dayTo = this.normalizeDay(data.dayTo); + this.monthMask = this.normalizeMonthMask(data.monthMask); + this.yearMode = this.normalizeMode(data.yearMode); + this.yearFrom = this.normalizeYear(data.yearFrom); + this.yearTo = this.normalizeYear(data.yearTo); + return; + } + + String[] data = wiredData.split("\t"); + if (data.length != 8) { + return; + } + + try { + this.weekdayMask = this.normalizeWeekdayMask(Integer.parseInt(data[0])); + this.dayMode = this.normalizeMode(Integer.parseInt(data[1])); + this.dayFrom = this.normalizeDay(Integer.parseInt(data[2])); + this.dayTo = this.normalizeDay(Integer.parseInt(data[3])); + this.monthMask = this.normalizeMonthMask(Integer.parseInt(data[4])); + this.yearMode = this.normalizeMode(Integer.parseInt(data[5])); + this.yearFrom = this.normalizeYear(Integer.parseInt(data[6])); + this.yearTo = this.normalizeYear(Integer.parseInt(data[7])); + } catch (NumberFormatException ignored) { + this.reset(); + } + } + + @Override + public void onPickUp() { + this.reset(); + } + + private void reset() { + int currentYear = HotelDateTimeUtil.localDateNow().getYear(); + + this.weekdayMask = ALL_WEEKDAYS_MASK; + this.dayMode = MODE_SKIP; + this.dayFrom = 1; + this.dayTo = 31; + this.monthMask = ALL_MONTHS_MASK; + this.yearMode = MODE_SKIP; + this.yearFrom = currentYear; + this.yearTo = currentYear; + } + + private boolean matchesMask(int value, int mask) { + return (mask & (1 << value)) != 0; + } + + private boolean matchesDatePart(int currentValue, int mode, int fromValue, int toValue) { + switch (mode) { + case MODE_EXACT: + return currentValue == fromValue; + case MODE_RANGE: + return currentValue >= Math.min(fromValue, toValue) && currentValue <= Math.max(fromValue, toValue); + default: + return true; + } + } + + private int normalizeMode(int value) { + if (value < MODE_SKIP || value > MODE_RANGE) { + return MODE_SKIP; + } + + return value; + } + + private int normalizeDay(int value) { + return Math.max(1, Math.min(31, value)); + } + + private int normalizeYear(int value) { + return Math.max(1, Math.min(9999, value)); + } + + private int normalizeWeekdayMask(int value) { + int normalized = value & ALL_WEEKDAYS_MASK; + return (normalized == 0) ? ALL_WEEKDAYS_MASK : normalized; + } + + private int normalizeMonthMask(int value) { + int normalized = value & ALL_MONTHS_MASK; + return (normalized == 0) ? ALL_MONTHS_MASK : normalized; + } + + private static int createMask(int startValue, int endValue) { + int mask = 0; + + for (int value = startValue; value <= endValue; value++) { + mask |= (1 << value); + } + + return mask; + } + + static class JsonData { + int weekdayMask; + int dayMode; + int dayFrom; + int dayTo; + int monthMask; + int yearMode; + int yearFrom; + int yearTo; + + public JsonData(int weekdayMask, int dayMode, int dayFrom, int dayTo, int monthMask, int yearMode, int yearFrom, int yearTo) { + this.weekdayMask = weekdayMask; + this.dayMode = dayMode; + this.dayFrom = dayFrom; + this.dayTo = dayTo; + this.monthMask = monthMask; + this.yearMode = yearMode; + this.yearFrom = yearFrom; + this.yearTo = yearTo; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java index bc965875..ddc0477e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java @@ -19,11 +19,13 @@ import gnu.trove.set.hash.THashSet; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; +import java.math.BigDecimal; public class WiredConditionMatchStatePosition extends InteractionWiredCondition implements InteractionWiredMatchFurniSettings { + protected static final int QUANTIFIER_ALL = 0; + protected static final int QUANTIFIER_ANY = 1; + public static final WiredConditionType type = WiredConditionType.MATCH_SSHOT; private THashSet settings; @@ -31,7 +33,9 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition private boolean state; private boolean position; private boolean direction; + private boolean altitude; private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private int quantifier = QUANTIFIER_ALL; public WiredConditionMatchStatePosition(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -62,11 +66,13 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(4); + message.appendInt(6); message.appendInt(this.state ? 1 : 0); message.appendInt(this.direction ? 1 : 0); message.appendInt(this.position ? 1 : 0); + message.appendInt(this.altitude ? 1 : 0); message.appendInt(this.furniSource); + message.appendInt(this.quantifier); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -80,7 +86,9 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition this.state = params[0] == 1; this.direction = params[1] == 1; this.position = params[2] == 1; - this.furniSource = (params.length > 3) ? params[3] : WiredSourceUtil.SOURCE_TRIGGER; + this.altitude = (params.length > 3) && (params[3] == 1); + this.furniSource = (params.length > 4) ? params[4] : ((params.length > 3 && params[3] > 1) ? params[3] : WiredSourceUtil.SOURCE_TRIGGER); + this.quantifier = (params.length > 5) ? this.normalizeQuantifier(params[5]) : QUANTIFIER_ALL; Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); @@ -92,14 +100,12 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition this.settings.clear(); - if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { - for (int i = 0; i < count; i++) { - int itemId = settings.getFurniIds()[i]; - HabboItem item = room.getHabboItem(itemId); + for (int i = 0; i < count; i++) { + int itemId = settings.getFurniIds()[i]; + HabboItem item = room.getHabboItem(itemId); - if (item != null) - this.settings.add(new WiredMatchFurniSetting(item.getId(), item.getExtradata(), item.getRotation(), item.getX(), item.getY())); - } + if (item != null) + this.settings.add(new WiredMatchFurniSetting(item.getId(), item.getExtradata(), item.getRotation(), item.getX(), item.getY(), item.getZ())); } return true; @@ -107,66 +113,119 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition @Override public boolean evaluate(WiredContext ctx) { - Room room = ctx.room(); + this.refresh(); + if (this.settings.isEmpty()) return true; - List targets = null; - Set targetIds = null; + if (this.quantifier == QUANTIFIER_ANY) { + return this.evaluateAnyTargetMatches(ctx); + } + + return this.evaluateAllTargetsMatch(ctx); + } + + protected boolean evaluateAllTargetsMatch(WiredContext ctx) { + Room room = ctx.room(); if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { - targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, null); + List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, null); if (targets.isEmpty()) return false; - targetIds = new HashSet<>(); - for (HabboItem item : targets) { - if (item != null) targetIds.add(item.getId()); - } - if (targetIds.isEmpty()) return false; - } - THashSet toRemove = new THashSet<>(); - Set settingsIds = new HashSet<>(); + for (HabboItem item : targets) { + if (item == null) return false; + + WiredMatchFurniSetting setting = this.resolveSettingForTarget(room, item); + if (setting == null) { + return false; + } + + if (!this.matchesSetting(item, setting)) { + return false; + } + } + + return true; + } for (WiredMatchFurniSetting setting : this.settings) { - if (targetIds != null && !targetIds.contains(setting.item_id)) { - continue; - } HabboItem item = room.getHabboItem(setting.item_id); - - if (item != null) { - settingsIds.add(setting.item_id); - if (this.state) { - if (!item.getExtradata().equals(setting.state)) - return false; - } - - if (this.position) { - if (!(setting.x == item.getX() && setting.y == item.getY())) - return false; - } - - if (this.direction) { - if (setting.rotation != item.getRotation()) - return false; - } - } else { - toRemove.add(setting); - } - } - - if (targetIds != null && !settingsIds.containsAll(targetIds)) { - return false; - } - - if (!toRemove.isEmpty()) { - for (WiredMatchFurniSetting setting : toRemove) { - this.settings.remove(setting); - } + if (item == null) continue; + if (!this.matchesSetting(item, setting)) + return false; } return true; } + protected boolean evaluateAnyTargetMatches(WiredContext ctx) { + Room room = ctx.room(); + + if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { + List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, null); + if (targets.isEmpty()) return false; + + for (HabboItem item : targets) { + if (item == null) continue; + + WiredMatchFurniSetting setting = this.resolveSettingForTarget(room, item); + if (setting != null && this.matchesSetting(item, setting)) { + return true; + } + } + + return false; + } + + for (WiredMatchFurniSetting setting : this.settings) { + HabboItem item = room.getHabboItem(setting.item_id); + if (item == null) continue; + + if (this.matchesSetting(item, setting)) { + return true; + } + } + + return false; + } + + protected int getQuantifier() { + return this.quantifier; + } + + private WiredMatchFurniSetting resolveSettingForTarget(Room room, HabboItem target) { + WiredMatchFurniSetting fallback = null; + + for (WiredMatchFurniSetting setting : this.settings) { + HabboItem sourceItem = room.getHabboItem(setting.item_id); + if (sourceItem == null) continue; + if (sourceItem.getBaseItem().getId() != target.getBaseItem().getId()) continue; + + if (setting.state.equals(target.getExtradata())) { + return setting; + } + + if (fallback == null) { + fallback = setting; + } + } + + return fallback; + } + + private boolean matchesSetting(HabboItem item, WiredMatchFurniSetting setting) { + if (this.state && !item.getExtradata().equals(setting.state)) + return false; + + if (this.position && !(setting.x == item.getX() && setting.y == item.getY())) + return false; + + if (this.altitude && BigDecimal.valueOf(item.getZ()).compareTo(BigDecimal.valueOf(setting.z)) != 0) + return false; + + return !this.direction || setting.rotation == item.getRotation(); + } + @Deprecated @Override public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { @@ -179,8 +238,10 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition this.state, this.position, this.direction, + this.altitude, new ArrayList<>(this.settings), - this.furniSource + this.furniSource, + this.quantifier )); } @@ -193,8 +254,12 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition this.state = data.state; this.position = data.position; this.direction = data.direction; - this.settings.addAll(data.settings); + this.altitude = data.altitude; + if (data.settings != null) { + this.settings.addAll(data.settings); + } this.furniSource = data.furniSource; + this.quantifier = this.normalizeQuantifier(data.quantifier); } else { String[] data = wiredData.split(":"); @@ -205,17 +270,18 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition for (int i = 0; i < itemCount; i++) { String[] stuff = items[i].split("-"); - if (stuff.length >= 5) + if (stuff.length >= 6) + this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]), Double.parseDouble(stuff[5]))); + else if (stuff.length >= 5) this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]))); } this.state = data[2].equals("1"); this.direction = data[3].equals("1"); this.position = data[4].equals("1"); + this.altitude = false; this.furniSource = this.settings.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; - } - if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.settings.isEmpty()) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + this.quantifier = QUANTIFIER_ALL; } } @@ -225,10 +291,16 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition this.direction = false; this.position = false; this.state = false; + this.altitude = false; this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; } - private void refresh() { + private int normalizeQuantifier(int value) { + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + + protected void refresh() { Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); if (room != null) { @@ -267,19 +339,28 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition return this.position; } + @Override + public boolean shouldMatchAltitude() { + return this.altitude; + } + static class JsonData { boolean state; boolean position; boolean direction; + boolean altitude; List settings; int furniSource; + int quantifier; - public JsonData(boolean state, boolean position, boolean direction, List settings, int furniSource) { + public JsonData(boolean state, boolean position, boolean direction, boolean altitude, List settings, int furniSource, int quantifier) { this.state = state; this.position = position; this.direction = direction; + this.altitude = altitude; this.settings = settings; this.furniSource = furniSource; + this.quantifier = quantifier; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchTime.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchTime.java new file mode 100644 index 00000000..982b56ec --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchTime.java @@ -0,0 +1,237 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.util.HotelDateTimeUtil; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalTime; + +public class WiredConditionMatchTime extends InteractionWiredCondition { + private static final int MODE_SKIP = 0; + private static final int MODE_EXACT = 1; + private static final int MODE_RANGE = 2; + + public static final WiredConditionType type = WiredConditionType.MATCH_TIME; + + private int hourMode = MODE_SKIP; + private int hourFrom = 0; + private int hourTo = 0; + private int minuteMode = MODE_SKIP; + private int minuteFrom = 0; + private int minuteTo = 0; + private int secondMode = MODE_SKIP; + private int secondFrom = 0; + private int secondTo = 0; + + public WiredConditionMatchTime(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionMatchTime(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(9); + message.appendInt(this.hourMode); + message.appendInt(this.hourFrom); + message.appendInt(this.hourTo); + message.appendInt(this.minuteMode); + message.appendInt(this.minuteFrom); + message.appendInt(this.minuteTo); + message.appendInt(this.secondMode); + message.appendInt(this.secondFrom); + message.appendInt(this.secondTo); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] params = settings.getIntParams(); + + this.hourMode = (params.length > 0) ? this.normalizeMode(params[0]) : MODE_SKIP; + this.hourFrom = (params.length > 1) ? this.normalizeHour(params[1]) : 0; + this.hourTo = (params.length > 2) ? this.normalizeHour(params[2]) : this.hourFrom; + this.minuteMode = (params.length > 3) ? this.normalizeMode(params[3]) : MODE_SKIP; + this.minuteFrom = (params.length > 4) ? this.normalizeMinuteOrSecond(params[4]) : 0; + this.minuteTo = (params.length > 5) ? this.normalizeMinuteOrSecond(params[5]) : this.minuteFrom; + this.secondMode = (params.length > 6) ? this.normalizeMode(params[6]) : MODE_SKIP; + this.secondFrom = (params.length > 7) ? this.normalizeMinuteOrSecond(params[7]) : 0; + this.secondTo = (params.length > 8) ? this.normalizeMinuteOrSecond(params[8]) : this.secondFrom; + + return true; + } + + @Override + public boolean evaluate(WiredContext ctx) { + LocalTime now = HotelDateTimeUtil.localTimeNow(); + + return this.matchesTimePart(now.getHour(), this.hourMode, this.hourFrom, this.hourTo) + && this.matchesTimePart(now.getMinute(), this.minuteMode, this.minuteFrom, this.minuteTo) + && this.matchesTimePart(now.getSecond(), this.secondMode, this.secondFrom, this.secondTo); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.hourMode, + this.hourFrom, + this.hourTo, + this.minuteMode, + this.minuteFrom, + this.minuteTo, + this.secondMode, + this.secondFrom, + this.secondTo + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.reset(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.hourMode = this.normalizeMode(data.hourMode); + this.hourFrom = this.normalizeHour(data.hourFrom); + this.hourTo = this.normalizeHour(data.hourTo); + this.minuteMode = this.normalizeMode(data.minuteMode); + this.minuteFrom = this.normalizeMinuteOrSecond(data.minuteFrom); + this.minuteTo = this.normalizeMinuteOrSecond(data.minuteTo); + this.secondMode = this.normalizeMode(data.secondMode); + this.secondFrom = this.normalizeMinuteOrSecond(data.secondFrom); + this.secondTo = this.normalizeMinuteOrSecond(data.secondTo); + return; + } + + String[] data = wiredData.split("\t"); + if (data.length != 9) { + return; + } + + try { + this.hourMode = this.normalizeMode(Integer.parseInt(data[0])); + this.hourFrom = this.normalizeHour(Integer.parseInt(data[1])); + this.hourTo = this.normalizeHour(Integer.parseInt(data[2])); + this.minuteMode = this.normalizeMode(Integer.parseInt(data[3])); + this.minuteFrom = this.normalizeMinuteOrSecond(Integer.parseInt(data[4])); + this.minuteTo = this.normalizeMinuteOrSecond(Integer.parseInt(data[5])); + this.secondMode = this.normalizeMode(Integer.parseInt(data[6])); + this.secondFrom = this.normalizeMinuteOrSecond(Integer.parseInt(data[7])); + this.secondTo = this.normalizeMinuteOrSecond(Integer.parseInt(data[8])); + } catch (NumberFormatException ignored) { + this.reset(); + } + } + + @Override + public void onPickUp() { + this.reset(); + } + + private void reset() { + this.hourMode = MODE_SKIP; + this.hourFrom = 0; + this.hourTo = 0; + this.minuteMode = MODE_SKIP; + this.minuteFrom = 0; + this.minuteTo = 0; + this.secondMode = MODE_SKIP; + this.secondFrom = 0; + this.secondTo = 0; + } + + private boolean matchesTimePart(int currentValue, int mode, int fromValue, int toValue) { + switch (mode) { + case MODE_EXACT: + return currentValue == fromValue; + case MODE_RANGE: + if (fromValue <= toValue) { + return currentValue >= fromValue && currentValue <= toValue; + } + + return currentValue >= fromValue || currentValue <= toValue; + default: + return true; + } + } + + private int normalizeMode(int value) { + if (value < MODE_SKIP || value > MODE_RANGE) { + return MODE_SKIP; + } + + return value; + } + + private int normalizeHour(int value) { + return Math.max(0, Math.min(23, value)); + } + + private int normalizeMinuteOrSecond(int value) { + return Math.max(0, Math.min(59, value)); + } + + static class JsonData { + int hourMode; + int hourFrom; + int hourTo; + int minuteMode; + int minuteFrom; + int minuteTo; + int secondMode; + int secondFrom; + int secondTo; + + public JsonData(int hourMode, int hourFrom, int hourTo, int minuteMode, int minuteFrom, int minuteTo, int secondMode, int secondFrom, int secondTo) { + this.hourMode = hourMode; + this.hourFrom = hourFrom; + this.hourTo = hourTo; + this.minuteMode = minuteMode; + this.minuteFrom = minuteFrom; + this.minuteTo = minuteTo; + this.secondMode = secondMode; + this.secondFrom = secondFrom; + this.secondTo = secondTo; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveFurni.java index c4540b26..41f72077 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveFurni.java @@ -177,6 +177,10 @@ public class WiredConditionNotFurniHaveFurni extends InteractionWiredCondition { int count = settings.getFurniIds().length; if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; + if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + this.items.clear(); if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveHabbo.java index dff23b8a..a7d85525 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveHabbo.java @@ -28,6 +28,7 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition { public static final WiredConditionType type = WiredConditionType.NOT_FURNI_HAVE_HABBO; protected THashSet items; + protected boolean all; private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; public WiredConditionNotFurniHaveHabbo(ResultSet set, Item baseItem) throws SQLException { @@ -43,6 +44,7 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition { @Override public void onPickUp() { this.items.clear(); + this.all = false; this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; } @@ -63,15 +65,11 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition { Collection bots = room.getCurrentBots().valueCollection(); Collection pets = room.getCurrentPets().valueCollection(); - return targets.stream().filter(item -> item != null).noneMatch(item -> { - RoomTile baseTile = room.getLayout().getTile(item.getX(), item.getY()); - if (baseTile == null) return false; - - THashSet occupiedTiles = room.getLayout().getTilesAt(baseTile, item.getBaseItem().getWidth(), item.getBaseItem().getLength(), item.getRotation()); - return habbos.stream().anyMatch(character -> character.getRoomUnit() != null && occupiedTiles.contains(character.getRoomUnit().getCurrentLocation())) || - bots.stream().anyMatch(character -> character.getRoomUnit() != null && occupiedTiles.contains(character.getRoomUnit().getCurrentLocation())) || - pets.stream().anyMatch(character -> character.getRoomUnit() != null && occupiedTiles.contains(character.getRoomUnit().getCurrentLocation())); - }); + if (this.all) { + return targets.stream().filter(item -> item != null).allMatch(item -> !this.hasAvatarOnItem(item, room, habbos, bots, pets)); + } + + return targets.stream().filter(item -> item != null).anyMatch(item -> !this.hasAvatarOnItem(item, room, habbos, bots, pets)); } @Deprecated @@ -85,7 +83,8 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition { this.refresh(); return WiredManager.getGson().toJson(new JsonData( this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), - this.furniSource + this.furniSource, + this.all )); } @@ -97,6 +96,7 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition { if (wiredData.startsWith("{")) { WiredConditionFurniHaveHabbo.JsonData data = WiredManager.getGson().fromJson(wiredData, WiredConditionFurniHaveHabbo.JsonData.class); this.furniSource = data.furniSource; + this.all = data.all; for(int id : data.itemIds) { HabboItem item = room.getHabboItem(id); @@ -119,6 +119,7 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition { } } this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; + this.all = false; } if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { this.furniSource = WiredSourceUtil.SOURCE_SELECTED; @@ -144,7 +145,8 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(1); + message.appendInt(2); + message.appendInt(this.all ? 1 : 0); message.appendInt(this.furniSource); message.appendInt(0); message.appendInt(this.getType().code); @@ -158,7 +160,12 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition { if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; int[] params = settings.getIntParams(); - this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.all = (params.length > 0) && (params[0] == 1); + this.furniSource = (params.length > 1) ? params[1] : ((params.length > 0 && params[0] > 1) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER); + + if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } this.items.clear(); @@ -178,6 +185,18 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition { return true; } + protected boolean hasAvatarOnItem(HabboItem item, Room room, Collection habbos, Collection bots, Collection pets) { + RoomTile baseTile = room.getLayout().getTile(item.getX(), item.getY()); + if (baseTile == null) return false; + + THashSet occupiedTiles = room.getLayout().getTilesAt(baseTile, item.getBaseItem().getWidth(), item.getBaseItem().getLength(), item.getRotation()); + return occupiedTiles != null && ( + habbos.stream().anyMatch(character -> character.getRoomUnit() != null && occupiedTiles.contains(character.getRoomUnit().getCurrentLocation())) || + bots.stream().anyMatch(character -> character.getRoomUnit() != null && occupiedTiles.contains(character.getRoomUnit().getCurrentLocation())) || + pets.stream().anyMatch(character -> character.getRoomUnit() != null && occupiedTiles.contains(character.getRoomUnit().getCurrentLocation())) + ); + } + private void refresh() { THashSet items = new THashSet<>(); @@ -199,10 +218,12 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition { static class JsonData { List itemIds; int furniSource; + boolean all; - public JsonData(List itemIds, int furniSource) { + public JsonData(List itemIds, int furniSource, boolean all) { this.itemIds = itemIds; this.furniSource = furniSource; + this.all = all; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniTypeMatch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniTypeMatch.java index b9aaf539..49dc6775 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniTypeMatch.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniTypeMatch.java @@ -1,30 +1,15 @@ package com.eu.habbo.habbohotel.items.interactions.wired.conditions; -import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.Item; -import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; -import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; -import com.eu.habbo.habbohotel.rooms.Room; -import com.eu.habbo.habbohotel.rooms.RoomUnit; -import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredConditionType; -import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; -import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; -import com.eu.habbo.messages.ServerMessage; -import gnu.trove.set.hash.THashSet; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.List; -import java.util.stream.Collectors; -public class WiredConditionNotFurniTypeMatch extends InteractionWiredCondition { +public class WiredConditionNotFurniTypeMatch extends WiredConditionFurniTypeMatch { public static final WiredConditionType type = WiredConditionType.NOT_STUFF_IS; - private THashSet items = new THashSet<>(); - private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; - public WiredConditionNotFurniTypeMatch(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); } @@ -35,145 +20,15 @@ public class WiredConditionNotFurniTypeMatch extends InteractionWiredCondition { @Override public boolean evaluate(WiredContext ctx) { - this.refresh(); - - if(items.isEmpty()) - return true; - - List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); - if (targets.isEmpty()) return true; - return targets.stream().noneMatch(this.items::contains); - } - - @Deprecated - @Override - public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { - return false; - } - - @Override - public String getWiredData() { - this.refresh(); - return WiredManager.getGson().toJson(new JsonData( - this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), - this.furniSource - )); - } - - @Override - public void loadWiredData(ResultSet set, Room room) throws SQLException { - this.items.clear(); - String wiredData = set.getString("wired_data"); - - if (wiredData.startsWith("{")) { - WiredConditionFurniTypeMatch.JsonData data = WiredManager.getGson().fromJson(wiredData, WiredConditionFurniTypeMatch.JsonData.class); - this.furniSource = data.furniSource; - - for(int id : data.itemIds) { - HabboItem item = room.getHabboItem(id); - - if (item != null) { - this.items.add(item); - } - } - } else { - String[] data = set.getString("wired_data").split(";"); - - for (String s : data) { - HabboItem item = room.getHabboItem(Integer.parseInt(s)); - - if (item != null) { - this.items.add(item); - } - } - this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; + if (this.getQuantifier() == QUANTIFIER_ANY) { + return !this.evaluateAllMatches(ctx); } - if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; - } - } - @Override - public void onPickUp() { - this.items.clear(); - this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + return !this.evaluateAnyMatches(ctx); } @Override public WiredConditionType getType() { return type; } - - @Override - public void serializeWiredData(ServerMessage message, Room room) { - this.refresh(); - - message.appendBoolean(false); - message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); - message.appendInt(this.items.size()); - - for (HabboItem item : this.items) - message.appendInt(item.getId()); - - message.appendInt(this.getBaseItem().getSpriteId()); - message.appendInt(this.getId()); - message.appendString(""); - message.appendInt(1); - message.appendInt(this.furniSource); - message.appendInt(0); - message.appendInt(this.getType().code); - message.appendInt(0); - message.appendInt(0); - } - - @Override - public boolean saveData(WiredSettings settings) { - int count = settings.getFurniIds().length; - if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; - - int[] params = settings.getIntParams(); - this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; - - this.items.clear(); - - if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { - Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); - - if (room != null) { - for (int i = 0; i < count; i++) { - this.items.add(room.getHabboItem(settings.getFurniIds()[i])); - } - } - } - - return true; - } - - private void refresh() { - THashSet items = new THashSet<>(); - - Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); - if (room == null) { - items.addAll(this.items); - } else { - for (HabboItem item : this.items) { - if (room.getHabboItem(item.getId()) == null) - items.add(item); - } - } - - for (HabboItem item : items) { - this.items.remove(item); - } - } - - static class JsonData { - List itemIds; - int furniSource; - - public JsonData(List itemIds, int furniSource) { - this.itemIds = itemIds; - this.furniSource = furniSource; - } - } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboHasEffect.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboHasEffect.java index d061c0a8..b7c72361 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboHasEffect.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboHasEffect.java @@ -1,26 +1,18 @@ package com.eu.habbo.habbohotel.items.interactions.wired.conditions; import com.eu.habbo.habbohotel.items.Item; -import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; -import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; -import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.wired.WiredConditionType; -import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; -import com.eu.habbo.messages.ServerMessage; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; -public class WiredConditionNotHabboHasEffect extends InteractionWiredCondition { +public class WiredConditionNotHabboHasEffect extends WiredConditionHabboHasEffect { private static final WiredConditionType type = WiredConditionType.NOT_ACTOR_WEARS_EFFECT; - protected int effectId; - private int userSource = WiredSourceUtil.SOURCE_TRIGGER; - public WiredConditionNotHabboHasEffect(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); } @@ -33,88 +25,16 @@ public class WiredConditionNotHabboHasEffect extends InteractionWiredCondition { public boolean evaluate(WiredContext ctx) { List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); if (targets.isEmpty()) return false; - for (RoomUnit roomUnit : targets) { - if (roomUnit == null) return false; - if (roomUnit.getEffectId() == this.effectId) { - return false; - } + + if (this.getQuantifier() == QUANTIFIER_ALL) { + return !this.matchesAllTargets(targets); } - return true; - } - @Deprecated - @Override - public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { - return false; - } - - @Override - public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData( - this.effectId, - this.userSource - )); - } - - @Override - public void loadWiredData(ResultSet set, Room room) throws SQLException { - String wiredData = set.getString("wired_data"); - - if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.effectId = data.effectId; - this.userSource = data.userSource; - } else { - this.effectId = Integer.parseInt(wiredData); - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; - } - } - - @Override - public void onPickUp() { - this.effectId = 0; - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + return !this.matchesAnyTarget(targets); } @Override public WiredConditionType getType() { return type; } - - @Override - public void serializeWiredData(ServerMessage message, Room room) { - message.appendBoolean(false); - message.appendInt(5); - message.appendInt(0); - message.appendInt(this.getBaseItem().getSpriteId()); - message.appendInt(this.getId()); - message.appendString(this.effectId + ""); - message.appendInt(2); - message.appendInt(this.effectId); - message.appendInt(this.userSource); - message.appendInt(0); - message.appendInt(this.getType().code); - message.appendInt(0); - message.appendInt(0); - } - - @Override - public boolean saveData(WiredSettings settings) { - if(settings.getIntParams().length < 1) return false; - this.effectId = settings.getIntParams()[0]; - int[] params = settings.getIntParams(); - this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; - - return true; - } - - static class JsonData { - int effectId; - int userSource; - - public JsonData(int effectId, int userSource) { - this.effectId = effectId; - this.userSource = userSource; - } - } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboHasHandItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboHasHandItem.java new file mode 100644 index 00000000..060b6089 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboHasHandItem.java @@ -0,0 +1,40 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +public class WiredConditionNotHabboHasHandItem extends WiredConditionHabboHasHandItem { + public static final WiredConditionType type = WiredConditionType.NOT_ACTOR_HAS_HANDITEM; + + public WiredConditionNotHabboHasHandItem(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionNotHabboHasHandItem(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean evaluate(WiredContext ctx) { + List targets = WiredSourceUtil.resolveUsers(ctx, this.getUserSource()); + if (targets.isEmpty()) return false; + + if (this.getQuantifier() == QUANTIFIER_ANY) { + return !this.matchesAnyTarget(targets); + } + + return !this.matchesAllTargets(targets); + } + + @Override + public WiredConditionType getType() { + return type; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboWearsBadge.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboWearsBadge.java index 5521d5d8..18a6c869 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboWearsBadge.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboWearsBadge.java @@ -1,28 +1,19 @@ package com.eu.habbo.habbohotel.items.interactions.wired.conditions; import com.eu.habbo.habbohotel.items.Item; -import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; -import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; 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.habbohotel.users.HabboBadge; import com.eu.habbo.habbohotel.wired.WiredConditionType; -import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; -import com.eu.habbo.messages.ServerMessage; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; -public class WiredConditionNotHabboWearsBadge extends InteractionWiredCondition { +public class WiredConditionNotHabboWearsBadge extends WiredConditionHabboWearsBadge { public static final WiredConditionType type = WiredConditionType.NOT_ACTOR_WEARS_BADGE; - protected String badge = ""; - private int userSource = WiredSourceUtil.SOURCE_TRIGGER; - public WiredConditionNotHabboWearsBadge(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); } @@ -37,91 +28,15 @@ public class WiredConditionNotHabboWearsBadge extends InteractionWiredCondition List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); if (targets.isEmpty()) return true; - for (RoomUnit roomUnit : targets) { - Habbo habbo = room.getHabbo(roomUnit); - if (habbo != null) { - synchronized (habbo.getInventory().getBadgesComponent().getWearingBadges()) { - for (HabboBadge b : habbo.getInventory().getBadgesComponent().getWearingBadges()) { - if (b.getCode().equalsIgnoreCase(this.badge)) - return false; - } - } - } + if (this.getQuantifier() == QUANTIFIER_ALL) { + return !this.matchesAllTargets(room, targets); } - return true; - } - @Deprecated - @Override - public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { - return false; - } - - @Override - public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData( - this.badge, - this.userSource - )); - } - - @Override - public void loadWiredData(ResultSet set, Room room) throws SQLException { - String wiredData = set.getString("wired_data"); - - if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.badge = data.badge; - this.userSource = data.userSource; - } else { - this.badge = wiredData; - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; - } - } - - @Override - public void onPickUp() { - this.badge = ""; - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + return !this.matchesAnyTarget(room, targets); } @Override public WiredConditionType getType() { return type; } - - @Override - public void serializeWiredData(ServerMessage message, Room room) { - message.appendBoolean(false); - message.appendInt(5); - message.appendInt(0); - message.appendInt(this.getBaseItem().getSpriteId()); - message.appendInt(this.getId()); - message.appendString(this.badge); - message.appendInt(1); - message.appendInt(this.userSource); - message.appendInt(0); - message.appendInt(this.getType().code); - message.appendInt(0); - message.appendInt(0); - } - - @Override - public boolean saveData(WiredSettings settings) { - this.badge = settings.getStringParam(); - int[] params = settings.getIntParams(); - this.userSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; - - return true; - } - - static class JsonData { - String badge; - int userSource; - - public JsonData(String badge, int userSource) { - this.badge = badge; - this.userSource = userSource; - } - } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHasVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHasVariable.java new file mode 100644 index 00000000..c0dcc43f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHasVariable.java @@ -0,0 +1,30 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredConditionNotHasVariable extends WiredConditionHasVariable { + public static final WiredConditionType type = WiredConditionType.NOT_HAS_VAR; + + public WiredConditionNotHasVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionNotHasVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public boolean evaluate(WiredContext ctx) { + return this.evaluateWithNegation(ctx, true); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotInGroup.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotInGroup.java index 237be63d..06055f4d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotInGroup.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotInGroup.java @@ -8,6 +8,7 @@ import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.wired.WiredConditionType; import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; @@ -16,8 +17,16 @@ import java.sql.SQLException; import java.util.List; public class WiredConditionNotInGroup extends InteractionWiredCondition { + private static final int GROUP_CURRENT_ROOM = 0; + private static final int GROUP_SELECTED = 1; + private static final int QUANTIFIER_ALL = 0; + private static final int QUANTIFIER_ANY = 1; + public static final WiredConditionType type = WiredConditionType.NOT_ACTOR_IN_GROUP; private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int groupType = GROUP_CURRENT_ROOM; + private int selectedGroupId = 0; + private int quantifier = QUANTIFIER_ALL; public WiredConditionNotInGroup(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -30,18 +39,31 @@ public class WiredConditionNotInGroup extends InteractionWiredCondition { @Override public boolean evaluate(WiredContext ctx) { Room room = ctx.room(); - if (room.getGuildId() == 0) + int targetGroupId = this.resolveTargetGroupId(room); + if (targetGroupId == 0) return false; List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); - if (targets.isEmpty()) return true; + if (targets.isEmpty()) return false; + + if (this.quantifier == QUANTIFIER_ANY) { + for (RoomUnit roomUnit : targets) { + Habbo habbo = room.getHabbo(roomUnit); + if (habbo == null || !habbo.getHabboStats().hasGuild(targetGroupId)) { + return true; + } + } + + return false; + } for (RoomUnit roomUnit : targets) { Habbo habbo = room.getHabbo(roomUnit); - if (habbo != null && habbo.getHabboStats().hasGuild(room.getGuildId())) { + if (habbo != null && habbo.getHabboStats().hasGuild(targetGroupId)) { return false; } } + return true; } @@ -53,26 +75,45 @@ public class WiredConditionNotInGroup extends InteractionWiredCondition { @Override public String getWiredData() { - return String.valueOf(this.userSource); + return WiredManager.getGson().toJson(new JsonData( + this.userSource, + this.groupType, + this.selectedGroupId, + this.quantifier + )); } @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.resetSettings(); + String wiredData = set.getString("wired_data"); - if (wiredData != null && !wiredData.isEmpty()) { - try { - this.userSource = Integer.parseInt(wiredData); - } catch (NumberFormatException ignored) { - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + try { + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.userSource = this.normalizeUserSource(data.userSource); + this.groupType = this.normalizeGroupType(data.groupType); + this.selectedGroupId = this.normalizeSelectedGroupId(data.selectedGroupId); + this.quantifier = this.normalizeQuantifier(data.quantifier); + return; } - } else { - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.userSource = this.normalizeUserSource(Integer.parseInt(wiredData)); + } catch (Exception ignored) { + this.resetSettings(); } } @Override public void onPickUp() { - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.resetSettings(); } @Override @@ -88,8 +129,11 @@ public class WiredConditionNotInGroup extends InteractionWiredCondition { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(1); + message.appendInt(4); message.appendInt(this.userSource); + message.appendInt(this.groupType); + message.appendInt(this.selectedGroupId); + message.appendInt(this.quantifier); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -99,7 +143,59 @@ public class WiredConditionNotInGroup extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { int[] params = settings.getIntParams(); - this.userSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.userSource = (params.length > 0) ? this.normalizeUserSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER; + this.groupType = (params.length > 1) ? this.normalizeGroupType(params[1]) : GROUP_CURRENT_ROOM; + this.selectedGroupId = (params.length > 2) ? this.normalizeSelectedGroupId(params[2]) : 0; + this.quantifier = (params.length > 3) ? this.normalizeQuantifier(params[3]) : QUANTIFIER_ALL; return true; } + + private void resetSettings() { + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.groupType = GROUP_CURRENT_ROOM; + this.selectedGroupId = 0; + this.quantifier = QUANTIFIER_ALL; + } + + private int resolveTargetGroupId(Room room) { + if (room == null) { + return 0; + } + + if (this.groupType == GROUP_SELECTED) { + return this.selectedGroupId; + } + + return room.getGuildId(); + } + + private int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private int normalizeGroupType(int value) { + return (value == GROUP_SELECTED) ? GROUP_SELECTED : GROUP_CURRENT_ROOM; + } + + private int normalizeSelectedGroupId(int value) { + return Math.max(0, value); + } + + private int normalizeQuantifier(int value) { + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + + static class JsonData { + int userSource; + int groupType; + int selectedGroupId; + int quantifier; + + public JsonData(int userSource, int groupType, int selectedGroupId, int quantifier) { + this.userSource = userSource; + this.groupType = groupType; + this.selectedGroupId = selectedGroupId; + this.quantifier = quantifier; + } + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotInTeam.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotInTeam.java index 9eead216..d2349c84 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotInTeam.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotInTeam.java @@ -1,28 +1,19 @@ package com.eu.habbo.habbohotel.items.interactions.wired.conditions; -import com.eu.habbo.habbohotel.games.GameTeamColors; import com.eu.habbo.habbohotel.items.Item; -import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; -import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; 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.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.WiredConditionType; -import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; -import com.eu.habbo.messages.ServerMessage; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; -public class WiredConditionNotInTeam extends InteractionWiredCondition { +public class WiredConditionNotInTeam extends WiredConditionTeamMember { public static final WiredConditionType type = WiredConditionType.NOT_ACTOR_IN_TEAM; - private GameTeamColors teamColor = GameTeamColors.RED; - private int userSource = WiredSourceUtil.SOURCE_TRIGGER; - public WiredConditionNotInTeam(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); } @@ -37,96 +28,15 @@ public class WiredConditionNotInTeam extends InteractionWiredCondition { List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); if (targets.isEmpty()) return true; - for (RoomUnit roomUnit : targets) { - Habbo habbo = room.getHabbo(roomUnit); - if (habbo != null && habbo.getHabboInfo().getGamePlayer() != null) { - if (habbo.getHabboInfo().getGamePlayer().getTeamColor().equals(this.teamColor)) { - return false; - } - } + if (this.getQuantifier() == QUANTIFIER_ALL) { + return !this.evaluateAllTargetsMatch(room, targets); } - return true; - } - @Deprecated - @Override - public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { - return false; - } - - @Override - public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData( - this.teamColor, - this.userSource - )); - } - - @Override - public void loadWiredData(ResultSet set, Room room) throws SQLException { - try { - String wiredData = set.getString("wired_data"); - - if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.teamColor = data.teamColor; - this.userSource = data.userSource; - } else { - if (!wiredData.equals("")) - this.teamColor = GameTeamColors.values()[Integer.parseInt(wiredData)]; - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; - } - } catch (Exception e) { - this.teamColor = GameTeamColors.RED; - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; - } - } - - @Override - public void onPickUp() { - this.teamColor = GameTeamColors.RED; - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + return !this.evaluateAnyTargetMatches(room, targets); } @Override public WiredConditionType getType() { return type; } - - @Override - public void serializeWiredData(ServerMessage message, Room room) { - message.appendBoolean(false); - message.appendInt(5); - message.appendInt(0); - message.appendInt(this.getBaseItem().getSpriteId()); - message.appendInt(this.getId()); - message.appendString(""); - message.appendInt(2); - message.appendInt(this.teamColor.type); - message.appendInt(this.userSource); - message.appendInt(0); - message.appendInt(this.getType().code); - message.appendInt(0); - message.appendInt(0); - } - - @Override - public boolean saveData(WiredSettings settings) { - if(settings.getIntParams().length < 1) return false; - this.teamColor = GameTeamColors.values()[settings.getIntParams()[0]]; - int[] params = settings.getIntParams(); - this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; - - return true; - } - - static class JsonData { - GameTeamColors teamColor; - int userSource; - - public JsonData(GameTeamColors teamColor, int userSource) { - this.teamColor = teamColor; - this.userSource = userSource; - } - } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotMatchStatePosition.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotMatchStatePosition.java index 2121bf97..e52315dc 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotMatchStatePosition.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotMatchStatePosition.java @@ -22,7 +22,17 @@ public class WiredConditionNotMatchStatePosition extends WiredConditionMatchStat @Override public boolean evaluate(WiredContext ctx) { - return !super.evaluate(ctx); + this.refresh(); + + if (this.getMatchFurniSettings().isEmpty()) { + return false; + } + + if (this.getQuantifier() == QUANTIFIER_ANY) { + return !this.evaluateAnyTargetMatches(ctx); + } + + return !this.evaluateAllTargetsMatch(ctx); } @Deprecated diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotTriggerOnFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotTriggerOnFurni.java index 4fe1d474..7b726e80 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotTriggerOnFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotTriggerOnFurni.java @@ -37,7 +37,11 @@ public class WiredConditionNotTriggerOnFurni extends WiredConditionTriggerOnFurn if (itemTargets.isEmpty()) return true; - return !isAnyUserOnFurni(userTargets, itemTargets, room); + if (this.getQuantifier() == QUANTIFIER_ANY) { + return !this.isAnyUserOnFurni(userTargets, itemTargets, room); + } + + return !this.areAllUsersOnFurni(userTargets, itemTargets, room); } @Deprecated diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotTriggererMatch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotTriggererMatch.java new file mode 100644 index 00000000..3e945329 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotTriggererMatch.java @@ -0,0 +1,31 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredConditionNotTriggererMatch extends WiredConditionTriggererMatch { + public static final WiredConditionType type = WiredConditionType.NOT_TRIGGERER_MATCH; + + public WiredConditionNotTriggererMatch(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionNotTriggererMatch(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean evaluate(WiredContext ctx) { + MatchResult result = this.evaluateMatch(ctx); + return result.valid && !result.matched; + } + + @Override + public WiredConditionType getType() { + return type; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotUserPerformsAction.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotUserPerformsAction.java new file mode 100644 index 00000000..d4ff4f38 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotUserPerformsAction.java @@ -0,0 +1,46 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +public class WiredConditionNotUserPerformsAction extends WiredConditionUserPerformsAction { + private static final int QUANTIFIER_ANY_NOT_MATCH = 0; + private static final int QUANTIFIER_NONE_MATCH = 1; + + public static final WiredConditionType type = WiredConditionType.NOT_USER_PERFORMS_ACTION; + + public WiredConditionNotUserPerformsAction(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionNotUserPerformsAction(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean evaluate(WiredContext ctx) { + List targets = WiredSourceUtil.resolveUsers(ctx, this.getUserSource()); + if (targets.isEmpty()) { + return false; + } + + if (this.getQuantifier() == QUANTIFIER_NONE_MATCH) { + return targets.stream().noneMatch(roomUnit -> this.matchesAction(ctx, roomUnit)); + } + + return targets.stream().anyMatch(roomUnit -> !this.matchesAction(ctx, roomUnit)); + } + + @Override + public WiredConditionType getType() { + return type; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java new file mode 100644 index 00000000..590ea9d8 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionSelectionQuantity.java @@ -0,0 +1,217 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +public class WiredConditionSelectionQuantity extends InteractionWiredCondition { + private static final int COMPARISON_LESS_THAN = 0; + private static final int COMPARISON_EQUAL = 1; + private static final int COMPARISON_GREATER_THAN = 2; + + private static final int SOURCE_GROUP_USERS = 0; + private static final int SOURCE_GROUP_FURNI = 1; + + public static final WiredConditionType type = WiredConditionType.SLC_QUANTITY; + + private int comparison = COMPARISON_EQUAL; + private int quantity = 0; + private int sourceGroup = SOURCE_GROUP_USERS; + private int sourceType = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredConditionSelectionQuantity(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionSelectionQuantity(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(4); + message.appendInt(this.comparison); + message.appendInt(this.quantity); + message.appendInt(this.sourceGroup); + message.appendInt(this.sourceType); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] params = settings.getIntParams(); + + this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL; + this.quantity = (params.length > 1) ? this.normalizeQuantity(params[1]) : 0; + this.sourceGroup = (params.length > 2) ? this.normalizeSourceGroup(params[2]) : SOURCE_GROUP_USERS; + this.sourceType = (params.length > 3) ? this.normalizeSourceType(this.sourceGroup, params[3]) : WiredSourceUtil.SOURCE_TRIGGER; + + return true; + } + + @Override + public boolean evaluate(WiredContext ctx) { + int count = this.resolveCount(ctx); + + switch (this.comparison) { + case COMPARISON_LESS_THAN: + return count < this.quantity; + case COMPARISON_GREATER_THAN: + return count > this.quantity; + default: + return count == this.quantity; + } + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.comparison, + this.quantity, + this.sourceGroup, + this.sourceType + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data == null) { + return; + } + + this.comparison = this.normalizeComparison(data.comparison); + this.quantity = this.normalizeQuantity(data.quantity); + this.sourceGroup = this.normalizeSourceGroup(data.sourceGroup); + this.sourceType = this.normalizeSourceType(this.sourceGroup, data.sourceType); + return; + } + + String[] parts = wiredData.split("\t"); + + try { + if (parts.length > 0) { + this.comparison = this.normalizeComparison(Integer.parseInt(parts[0])); + } + if (parts.length > 1) { + this.quantity = this.normalizeQuantity(Integer.parseInt(parts[1])); + } + if (parts.length > 2) { + this.sourceGroup = this.normalizeSourceGroup(Integer.parseInt(parts[2])); + } + if (parts.length > 3) { + this.sourceType = this.normalizeSourceType(this.sourceGroup, Integer.parseInt(parts[3])); + } + } catch (NumberFormatException ignored) { + this.onPickUp(); + } + } + + @Override + public void onPickUp() { + this.comparison = COMPARISON_EQUAL; + this.quantity = 0; + this.sourceGroup = SOURCE_GROUP_USERS; + this.sourceType = WiredSourceUtil.SOURCE_TRIGGER; + } + + private int resolveCount(WiredContext ctx) { + if (this.sourceGroup == SOURCE_GROUP_FURNI) { + List items = WiredSourceUtil.resolveItems(ctx, this.sourceType, null); + + return items.size(); + } + + List users = WiredSourceUtil.resolveUsers(ctx, this.sourceType); + + return users.size(); + } + + private int normalizeComparison(int value) { + switch (value) { + case COMPARISON_LESS_THAN: + case COMPARISON_GREATER_THAN: + return value; + default: + return COMPARISON_EQUAL; + } + } + + private int normalizeQuantity(int value) { + return Math.max(0, Math.min(100, value)); + } + + private int normalizeSourceGroup(int value) { + return (value == SOURCE_GROUP_FURNI) ? SOURCE_GROUP_FURNI : SOURCE_GROUP_USERS; + } + + private int normalizeSourceType(int group, int value) { + if (group == SOURCE_GROUP_USERS) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + switch (value) { + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_TRIGGER: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + static class JsonData { + int comparison; + int quantity; + int sourceGroup; + int sourceType; + + public JsonData(int comparison, int quantity, int sourceGroup, int sourceType) { + this.comparison = comparison; + this.quantity = quantity; + this.sourceGroup = sourceGroup; + this.sourceType = sourceType; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBase.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBase.java new file mode 100644 index 00000000..1b5dd1d9 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBase.java @@ -0,0 +1,190 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GameState; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame; +import com.eu.habbo.habbohotel.games.freeze.FreezeGame; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; +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.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +abstract class WiredConditionTeamGameBase extends InteractionWiredCondition { + protected static final int QUANTIFIER_ALL = 0; + protected static final int QUANTIFIER_ANY = 1; + protected static final int COMPARISON_LOWER = 0; + protected static final int COMPARISON_EQUAL = 1; + protected static final int COMPARISON_HIGHER = 2; + protected static final int TEAM_TRIGGERER = 0; + + private static final GameTeamColors[] SUPPORTED_TEAM_COLORS = new GameTeamColors[] { + GameTeamColors.RED, + GameTeamColors.GREEN, + GameTeamColors.BLUE, + GameTeamColors.YELLOW + }; + + protected WiredConditionTeamGameBase(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + protected WiredConditionTeamGameBase(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + protected List resolveUsers(WiredContext ctx, int userSource) { + Map deduplicated = new LinkedHashMap<>(); + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, userSource)) { + if (roomUnit != null) { + deduplicated.putIfAbsent(roomUnit.getId(), roomUnit); + } + } + + return new ArrayList<>(deduplicated.values()); + } + + protected boolean matchesQuantifier(List users, int quantifier, Predicate predicate) { + if (users.isEmpty()) { + return false; + } + + if (quantifier == QUANTIFIER_ANY) { + return users.stream().anyMatch(predicate); + } + + return users.stream().allMatch(predicate); + } + + protected int normalizeQuantifier(int value) { + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + + protected int normalizeComparison(int value) { + switch (value) { + case COMPARISON_LOWER: + case COMPARISON_HIGHER: + return value; + default: + return COMPARISON_EQUAL; + } + } + + protected int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + protected int normalizePlacement(int value) { + if (value < 1 || value > 4) { + return 1; + } + + return value; + } + + protected int normalizeScore(int value) { + return Math.max(0, value); + } + + protected int normalizeExplicitTeamType(int value) { + GameTeamColors color = GameTeamColors.fromType(value); + return (color.type >= GameTeamColors.RED.type && color.type <= GameTeamColors.YELLOW.type) + ? color.type + : GameTeamColors.RED.type; + } + + protected int normalizeRankTeamType(int value) { + if (value == TEAM_TRIGGERER) { + return TEAM_TRIGGERER; + } + + return this.normalizeExplicitTeamType(value); + } + + protected GameTeamColors resolveConfiguredTeamColor(int value) { + return GameTeamColors.fromType(this.normalizeExplicitTeamType(value)); + } + + protected boolean compareValue(int actual, int expected, int comparison) { + switch (comparison) { + case COMPARISON_LOWER: + return actual < expected; + case COMPARISON_HIGHER: + return actual > expected; + default: + return actual == expected; + } + } + + protected UserGameContext resolveUserGameContext(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) { + return null; + } + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo == null || habbo.getHabboInfo() == null || habbo.getHabboInfo().getCurrentGame() == null) { + return null; + } + + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (!this.isSupportedGame(game)) { + return null; + } + + GameTeam team = game.getTeamForHabbo(habbo); + if (team == null) { + return null; + } + + return new UserGameContext(habbo, game, team); + } + + protected int getTeamRank(Game game, GameTeam team) { + if (game == null || team == null) { + return Integer.MAX_VALUE; + } + + int rank = 1; + int targetScore = team.getTotalScore(); + + for (GameTeamColors teamColor : SUPPORTED_TEAM_COLORS) { + GameTeam otherTeam = game.getTeam(teamColor); + if (otherTeam != null && otherTeam != team && otherTeam.getTotalScore() > targetScore) { + rank++; + } + } + + return rank; + } + + private boolean isSupportedGame(Game game) { + return game != null + && game.getState() != GameState.IDLE + && (game instanceof FreezeGame || game instanceof BattleBanzaiGame); + } + + protected static class UserGameContext { + protected final Habbo habbo; + protected final Game game; + protected final GameTeam team; + + protected UserGameContext(Habbo habbo, Game game, GameTeam team) { + this.habbo = habbo; + this.game = game; + this.team = team; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamHasRank.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamHasRank.java new file mode 100644 index 00000000..58272197 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamHasRank.java @@ -0,0 +1,175 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +public class WiredConditionTeamHasRank extends WiredConditionTeamGameBase { + public static final WiredConditionType type = WiredConditionType.TEAM_HAS_RANK; + + private int teamType = GameTeamColors.RED.type; + private int placement = 1; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int quantifier = QUANTIFIER_ALL; + + public WiredConditionTeamHasRank(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionTeamHasRank(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean evaluate(WiredContext ctx) { + Room room = ctx.room(); + List users = this.resolveUsers(ctx, this.userSource); + + return this.matchesQuantifier(users, this.quantifier, roomUnit -> this.matchesUser(ctx, room, roomUnit)); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.teamType, + this.placement, + this.userSource, + this.quantifier + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.resetSettings(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.teamType = this.normalizeRankTeamType(data.teamType); + this.placement = this.normalizePlacement(data.placement); + this.userSource = this.normalizeUserSource(data.userSource); + this.quantifier = this.normalizeQuantifier(data.quantifier); + } + + @Override + public void onPickUp() { + this.resetSettings(); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(4); + message.appendInt(this.teamType); + message.appendInt(this.placement); + message.appendInt(this.userSource); + message.appendInt(this.quantifier); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] params = settings.getIntParams(); + this.resetSettings(); + + if (params.length > 0) this.teamType = this.normalizeRankTeamType(params[0]); + if (params.length > 1) this.placement = this.normalizePlacement(params[1]); + if (params.length > 2) this.userSource = this.normalizeUserSource(params[2]); + if (params.length > 3) this.quantifier = this.normalizeQuantifier(params[3]); + + return true; + } + + private boolean matchesUser(WiredContext ctx, Room room, RoomUnit roomUnit) { + UserGameContext context = this.resolveUserGameContext(room, roomUnit); + if (context == null) { + return false; + } + + GameTeamColors requiredTeam = this.resolveRequiredTeamColor(ctx, room, context.game); + if (requiredTeam == GameTeamColors.NONE || context.team.teamColor != requiredTeam) { + return false; + } + + GameTeam team = context.game.getTeam(requiredTeam); + if (team == null) { + return false; + } + + return this.getTeamRank(context.game, team) == this.placement; + } + + private GameTeamColors resolveRequiredTeamColor(WiredContext ctx, Room room, com.eu.habbo.habbohotel.games.Game game) { + if (this.teamType == TEAM_TRIGGERER) { + RoomUnit actor = ctx.actor().orElse(null); + UserGameContext triggererContext = this.resolveUserGameContext(room, actor); + + if (triggererContext == null || triggererContext.game != game) { + return GameTeamColors.NONE; + } + + return triggererContext.team.teamColor; + } + + return this.resolveConfiguredTeamColor(this.teamType); + } + + private void resetSettings() { + this.teamType = GameTeamColors.RED.type; + this.placement = 1; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; + } + + static class JsonData { + int teamType; + int placement; + int userSource; + int quantifier; + + public JsonData(int teamType, int placement, int userSource, int quantifier) { + this.teamType = teamType; + this.placement = placement; + this.userSource = userSource; + this.quantifier = quantifier; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamHasScore.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamHasScore.java new file mode 100644 index 00000000..e85741a6 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamHasScore.java @@ -0,0 +1,162 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +public class WiredConditionTeamHasScore extends WiredConditionTeamGameBase { + public static final WiredConditionType type = WiredConditionType.TEAM_HAS_SCORE; + + private int teamType = GameTeamColors.RED.type; + private int comparison = COMPARISON_EQUAL; + private int score = 0; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int quantifier = QUANTIFIER_ALL; + + public WiredConditionTeamHasScore(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionTeamHasScore(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean evaluate(WiredContext ctx) { + Room room = ctx.room(); + List users = this.resolveUsers(ctx, this.userSource); + + return this.matchesQuantifier(users, this.quantifier, roomUnit -> this.matchesUser(room, roomUnit)); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.teamType, + this.comparison, + this.score, + this.userSource, + this.quantifier + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.resetSettings(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.teamType = this.normalizeExplicitTeamType(data.teamType); + this.comparison = this.normalizeComparison(data.comparison); + this.score = this.normalizeScore(data.score); + this.userSource = this.normalizeUserSource(data.userSource); + this.quantifier = this.normalizeQuantifier(data.quantifier); + } + + @Override + public void onPickUp() { + this.resetSettings(); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(5); + message.appendInt(this.teamType); + message.appendInt(this.comparison); + message.appendInt(this.score); + message.appendInt(this.userSource); + message.appendInt(this.quantifier); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] params = settings.getIntParams(); + this.resetSettings(); + + if (params.length > 0) this.teamType = this.normalizeExplicitTeamType(params[0]); + if (params.length > 1) this.comparison = this.normalizeComparison(params[1]); + if (params.length > 2) this.score = this.normalizeScore(params[2]); + if (params.length > 3) this.userSource = this.normalizeUserSource(params[3]); + if (params.length > 4) this.quantifier = this.normalizeQuantifier(params[4]); + + return true; + } + + private boolean matchesUser(Room room, RoomUnit roomUnit) { + UserGameContext context = this.resolveUserGameContext(room, roomUnit); + if (context == null) { + return false; + } + + GameTeamColors requiredTeam = this.resolveConfiguredTeamColor(this.teamType); + if (context.team.teamColor != requiredTeam) { + return false; + } + + return this.compareValue(context.team.getTotalScore(), this.score, this.comparison); + } + + private void resetSettings() { + this.teamType = GameTeamColors.RED.type; + this.comparison = COMPARISON_EQUAL; + this.score = 0; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; + } + + static class JsonData { + int teamType; + int comparison; + int score; + int userSource; + int quantifier; + + public JsonData(int teamType, int comparison, int score, int userSource, int quantifier) { + this.teamType = teamType; + this.comparison = comparison; + this.score = score; + this.userSource = userSource; + this.quantifier = quantifier; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamMember.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamMember.java index 658f8b7d..4ca7b4e1 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamMember.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamMember.java @@ -18,10 +18,14 @@ import java.sql.SQLException; import java.util.List; public class WiredConditionTeamMember extends InteractionWiredCondition { + protected static final int QUANTIFIER_ALL = 0; + protected static final int QUANTIFIER_ANY = 1; + public static final WiredConditionType type = WiredConditionType.ACTOR_IN_TEAM; private GameTeamColors teamColor = GameTeamColors.RED; - private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int userSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int quantifier = QUANTIFIER_ALL; public WiredConditionTeamMember(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -37,17 +41,40 @@ public class WiredConditionTeamMember extends InteractionWiredCondition { List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); if (targets.isEmpty()) return false; + if (this.quantifier == QUANTIFIER_ANY) { + return this.evaluateAnyTargetMatches(room, targets); + } + + return this.evaluateAllTargetsMatch(room, targets); + } + + protected boolean evaluateAllTargetsMatch(Room room, List targets) { for (RoomUnit roomUnit : targets) { - Habbo habbo = room.getHabbo(roomUnit); - if (habbo != null && habbo.getHabboInfo().getGamePlayer() != null) { - if (habbo.getHabboInfo().getGamePlayer().getTeamColor().equals(this.teamColor)) { - return true; - } + if (!this.matchesTeam(room, roomUnit)) { + return false; } } + + return true; + } + + protected boolean evaluateAnyTargetMatches(Room room, List targets) { + for (RoomUnit roomUnit : targets) { + if (this.matchesTeam(room, roomUnit)) { + return true; + } + } + return false; } + protected boolean matchesTeam(Room room, RoomUnit roomUnit) { + Habbo habbo = room.getHabbo(roomUnit); + return habbo != null + && habbo.getHabboInfo().getGamePlayer() != null + && habbo.getHabboInfo().getGamePlayer().getTeamColor().equals(this.teamColor); + } + @Deprecated @Override public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { @@ -58,7 +85,8 @@ public class WiredConditionTeamMember extends InteractionWiredCondition { public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( this.teamColor, - this.userSource + this.userSource, + this.quantifier )); } @@ -71,14 +99,17 @@ public class WiredConditionTeamMember extends InteractionWiredCondition { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.teamColor = data.teamColor; this.userSource = data.userSource; + this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY); } else { if (!wiredData.equals("")) this.teamColor = GameTeamColors.values()[Integer.parseInt(wiredData)]; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ANY; } } catch (Exception e) { this.teamColor = GameTeamColors.RED; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; } } @@ -86,6 +117,7 @@ public class WiredConditionTeamMember extends InteractionWiredCondition { public void onPickUp() { this.teamColor = GameTeamColors.RED; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; } @Override @@ -101,9 +133,10 @@ public class WiredConditionTeamMember extends InteractionWiredCondition { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(2); + message.appendInt(3); message.appendInt(this.teamColor.type); message.appendInt(this.userSource); + message.appendInt(this.quantifier); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -113,20 +146,35 @@ public class WiredConditionTeamMember extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 1) return false; - this.teamColor = GameTeamColors.values()[settings.getIntParams()[0]]; int[] params = settings.getIntParams(); + this.teamColor = GameTeamColors.values()[params[0]]; this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2], QUANTIFIER_ALL) : QUANTIFIER_ANY; return true; } + protected int getQuantifier() { + return this.quantifier; + } + + protected int normalizeQuantifier(Integer value, int fallback) { + if (value == null) { + return fallback; + } + + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + static class JsonData { GameTeamColors teamColor; int userSource; + Integer quantifier; - public JsonData(GameTeamColors teamColor, int userSource) { + public JsonData(GameTeamColors teamColor, int userSource, int quantifier) { this.teamColor = teamColor; this.userSource = userSource; + this.quantifier = quantifier; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggerOnFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggerOnFurni.java index 560ac2d8..82054911 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggerOnFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggerOnFurni.java @@ -22,11 +22,15 @@ import java.util.List; import java.util.stream.Collectors; public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { + protected static final int QUANTIFIER_ALL = 0; + protected static final int QUANTIFIER_ANY = 1; + public static final WiredConditionType type = WiredConditionType.TRIGGER_ON_FURNI; protected THashSet items = new THashSet<>(); protected int furniSource = WiredSourceUtil.SOURCE_TRIGGER; protected int userSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int quantifier = QUANTIFIER_ALL; public WiredConditionTriggerOnFurni(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -38,8 +42,6 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { @Override public boolean evaluate(WiredContext ctx) { - Room room = ctx.room(); - this.refresh(); List userTargets = WiredSourceUtil.resolveUsers(ctx, this.userSource); @@ -50,7 +52,11 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { if (itemTargets.isEmpty()) return false; - return isAnyUserOnFurni(userTargets, itemTargets, room); + if (this.quantifier == QUANTIFIER_ANY) { + return this.isAnyUserOnFurni(userTargets, itemTargets, ctx.room()); + } + + return this.areAllUsersOnFurni(userTargets, itemTargets, ctx.room()); } @Deprecated @@ -70,13 +76,29 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { return false; } + protected boolean areAllUsersOnFurni(Collection users, Collection items, Room room) { + for (RoomUnit roomUnit : users) { + if (roomUnit == null) { + return false; + } + + THashSet itemsAtUser = room.getItemsAt(roomUnit.getCurrentLocation()); + if (itemsAtUser == null || items.stream().noneMatch(itemsAtUser::contains)) { + return false; + } + } + + return true; + } + @Override public String getWiredData() { this.refresh(); return WiredManager.getGson().toJson(new JsonData( this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), this.furniSource, - this.userSource + this.userSource, + this.quantifier )); } @@ -89,6 +111,7 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.furniSource = data.furniSource; this.userSource = data.userSource; + this.quantifier = this.normalizeQuantifier(data.quantifier); for(int id : data.itemIds) { HabboItem item = room.getHabboItem(id); @@ -109,6 +132,7 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { } this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; } if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { this.furniSource = WiredSourceUtil.SOURCE_SELECTED; @@ -120,6 +144,7 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { this.items.clear(); this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; } @Override @@ -141,9 +166,10 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(2); + message.appendInt(3); message.appendInt(this.furniSource); message.appendInt(this.userSource); + message.appendInt(this.quantifier); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -158,6 +184,11 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { int[] params = settings.getIntParams(); this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL; + + if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } this.items.clear(); @@ -194,6 +225,14 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { this.items.removeAll(items); } + protected int getQuantifier() { + return this.quantifier; + } + + protected int normalizeQuantifier(int value) { + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + @Override public WiredConditionOperator operator() { return WiredConditionOperator.AND; @@ -203,11 +242,13 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { List itemIds; int furniSource; int userSource; + int quantifier; - public JsonData(List itemIds, int furniSource, int userSource) { + public JsonData(List itemIds, int furniSource, int userSource, int quantifier) { this.itemIds = itemIds; this.furniSource = furniSource; this.userSource = userSource; + this.quantifier = quantifier; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggererMatch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggererMatch.java new file mode 100644 index 00000000..e7f01dcb --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggererMatch.java @@ -0,0 +1,359 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitType; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class WiredConditionTriggererMatch extends InteractionWiredCondition { + protected static final int ENTITY_HABBO = 1; + protected static final int ENTITY_PET = 2; + protected static final int ENTITY_BOT = 4; + protected static final int AVATAR_MODE_ANY = 0; + protected static final int AVATAR_MODE_CERTAIN = 1; + protected static final int QUANTIFIER_ALL = 0; + protected static final int QUANTIFIER_ANY = 1; + protected static final int SOURCE_SPECIFIED_USERNAME = 101; + + public static final WiredConditionType type = WiredConditionType.TRIGGERER_MATCH; + + private int entityType = ENTITY_HABBO; + private int avatarMode = AVATAR_MODE_ANY; + private int matchUserSource = WiredSourceUtil.SOURCE_TRIGGER; + private int compareUserSource = WiredSourceUtil.SOURCE_TRIGGER; + private int quantifier = QUANTIFIER_ALL; + private String username = ""; + + public WiredConditionTriggererMatch(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionTriggererMatch(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean evaluate(WiredContext ctx) { + MatchResult result = this.evaluateMatch(ctx); + return result.valid && result.matched; + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.entityType, + this.avatarMode, + this.matchUserSource, + this.compareUserSource, + this.quantifier, + this.username + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.resetSettings(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.entityType = this.normalizeEntityType(data.entityType); + this.avatarMode = this.normalizeAvatarMode(data.avatarMode); + this.matchUserSource = this.normalizePrimaryUserSource(data.matchUserSource); + this.compareUserSource = this.normalizeCompareUserSource(data.compareUserSource); + this.quantifier = this.normalizeQuantifier(data.quantifier); + this.username = this.normalizeUsername(data.username); + } + + @Override + public void onPickUp() { + this.resetSettings(); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(true); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.username); + message.appendInt(5); + message.appendInt(this.entityType); + message.appendInt(this.avatarMode); + message.appendInt(this.matchUserSource); + message.appendInt(this.compareUserSource); + message.appendInt(this.quantifier); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] params = settings.getIntParams(); + + this.resetSettings(); + + if (params.length > 0) this.entityType = this.normalizeEntityType(params[0]); + if (params.length > 1) this.avatarMode = this.normalizeAvatarMode(params[1]); + if (params.length > 2) this.matchUserSource = this.normalizePrimaryUserSource(params[2]); + if (params.length > 3) this.compareUserSource = this.normalizeCompareUserSource(params[3]); + if (params.length > 4) this.quantifier = this.normalizeQuantifier(params[4]); + + this.username = this.normalizeUsername(settings.getStringParam()); + + return true; + } + + protected MatchResult evaluateMatch(WiredContext ctx) { + List matchUsers = this.resolvePrimaryUsers(ctx); + if (matchUsers.isEmpty()) { + return MatchResult.invalid(); + } + + List compareUsers = this.resolveCompareUsers(ctx); + if (compareUsers.isEmpty()) { + return MatchResult.valid(false); + } + + Set compareUserIds = compareUsers.stream() + .filter(this::matchesEntityType) + .map(RoomUnit::getId) + .collect(Collectors.toSet()); + + if (compareUserIds.isEmpty()) { + return MatchResult.valid(false); + } + + boolean matched; + if (this.quantifier == QUANTIFIER_ANY) { + matched = matchUsers.stream().anyMatch(roomUnit -> this.matchesCandidate(roomUnit, compareUserIds)); + } else { + matched = matchUsers.stream().allMatch(roomUnit -> this.matchesCandidate(roomUnit, compareUserIds)); + } + + return MatchResult.valid(matched); + } + + protected int getQuantifier() { + return this.quantifier; + } + + private void resetSettings() { + this.entityType = ENTITY_HABBO; + this.avatarMode = AVATAR_MODE_ANY; + this.matchUserSource = WiredSourceUtil.SOURCE_TRIGGER; + this.compareUserSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; + this.username = ""; + } + + private List resolvePrimaryUsers(WiredContext ctx) { + return this.deduplicate(WiredSourceUtil.resolveUsers(ctx, this.matchUserSource)); + } + + private List resolveCompareUsers(WiredContext ctx) { + List resolved; + + if (this.compareUserSource == SOURCE_SPECIFIED_USERNAME) { + resolved = this.resolveUsersByName(ctx.room(), this.username); + } else { + resolved = WiredSourceUtil.resolveUsers(ctx, this.compareUserSource); + } + + if (this.avatarMode == AVATAR_MODE_CERTAIN) { + String normalizedName = this.normalizeUsername(this.username); + if (normalizedName.isEmpty()) { + return new ArrayList<>(); + } + + resolved = resolved.stream() + .filter(roomUnit -> normalizedName.equalsIgnoreCase(this.getRoomUnitName(ctx.room(), roomUnit))) + .collect(Collectors.toList()); + } + + return this.deduplicate(resolved); + } + + private List resolveUsersByName(Room room, String username) { + List result = new ArrayList<>(); + String normalizedName = this.normalizeUsername(username); + if (room == null || normalizedName.isEmpty()) { + return result; + } + + Habbo habbo = room.getHabbo(normalizedName); + if (habbo != null && habbo.getRoomUnit() != null) { + result.add(habbo.getRoomUnit()); + } + + for (Bot bot : room.getBots(normalizedName)) { + if (bot != null && bot.getRoomUnit() != null) { + result.add(bot.getRoomUnit()); + } + } + + for (Pet pet : room.getUnitManager().getPets()) { + if (pet != null && pet.getRoomUnit() != null && normalizedName.equalsIgnoreCase(pet.getName())) { + result.add(pet.getRoomUnit()); + } + } + + return result; + } + + private List deduplicate(List users) { + Map deduplicated = new LinkedHashMap<>(); + + for (RoomUnit user : users) { + if (user != null) { + deduplicated.putIfAbsent(user.getId(), user); + } + } + + return new ArrayList<>(deduplicated.values()); + } + + private boolean matchesCandidate(RoomUnit roomUnit, Set compareUserIds) { + return roomUnit != null && this.matchesEntityType(roomUnit) && compareUserIds.contains(roomUnit.getId()); + } + + private boolean matchesEntityType(RoomUnit roomUnit) { + return roomUnit != null && roomUnit.getRoomUnitType().getTypeId() == this.entityType; + } + + private String getRoomUnitName(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) { + return ""; + } + + if (roomUnit.getRoomUnitType() == RoomUnitType.USER) { + Habbo habbo = room.getHabbo(roomUnit); + return (habbo != null && habbo.getHabboInfo() != null) ? habbo.getHabboInfo().getUsername() : ""; + } + + if (roomUnit.getRoomUnitType() == RoomUnitType.BOT) { + Bot bot = room.getBot(roomUnit); + return (bot != null) ? bot.getName() : ""; + } + + if (roomUnit.getRoomUnitType() == RoomUnitType.PET) { + Pet pet = room.getPet(roomUnit); + return (pet != null) ? pet.getName() : ""; + } + + return ""; + } + + private int normalizeEntityType(int value) { + switch (value) { + case ENTITY_HABBO: + case ENTITY_PET: + case ENTITY_BOT: + return value; + default: + return ENTITY_HABBO; + } + } + + private int normalizeAvatarMode(int value) { + return (value == AVATAR_MODE_CERTAIN) ? AVATAR_MODE_CERTAIN : AVATAR_MODE_ANY; + } + + private int normalizeQuantifier(int value) { + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + + private int normalizePrimaryUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private int normalizeCompareUserSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_CLICKED_USER: + case SOURCE_SPECIFIED_USERNAME: + return value; + default: + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + } + + private String normalizeUsername(String value) { + return (value == null) ? "" : value.trim(); + } + + protected static class MatchResult { + protected final boolean valid; + protected final boolean matched; + + private MatchResult(boolean valid, boolean matched) { + this.valid = valid; + this.matched = matched; + } + + private static MatchResult invalid() { + return new MatchResult(false, false); + } + + private static MatchResult valid(boolean matched) { + return new MatchResult(true, matched); + } + } + + static class JsonData { + int entityType; + int avatarMode; + int matchUserSource; + int compareUserSource; + int quantifier; + String username; + + public JsonData(int entityType, int avatarMode, int matchUserSource, int compareUserSource, int quantifier, String username) { + this.entityType = entityType; + this.avatarMode = avatarMode; + this.matchUserSource = matchUserSource; + this.compareUserSource = compareUserSource; + this.quantifier = quantifier; + this.username = username; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPerformsAction.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPerformsAction.java new file mode 100644 index 00000000..5e49a6a1 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPerformsAction.java @@ -0,0 +1,339 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.WiredUserActionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +public class WiredConditionUserPerformsAction extends InteractionWiredCondition { + private static final String CACHE_LAST_ACTION_ID = "wired.last_user_action.id"; + private static final String CACHE_LAST_ACTION_PARAMETER = "wired.last_user_action.parameter"; + private static final String CACHE_LAST_ACTION_TIMESTAMP = "wired.last_user_action.timestamp"; + private static final long TRANSIENT_ACTION_WINDOW_MS = 5_000L; + protected static final int DEFAULT_ACTION = WiredUserActionType.WAVE; + protected static final int QUANTIFIER_ALL = 0; + protected static final int QUANTIFIER_ANY = 1; + + public static final WiredConditionType type = WiredConditionType.USER_PERFORMS_ACTION; + + private int selectedAction = DEFAULT_ACTION; + private boolean signFilterEnabled = false; + private int signId = 0; + private boolean danceFilterEnabled = false; + private int danceId = 1; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int quantifier = QUANTIFIER_ALL; + + public WiredConditionUserPerformsAction(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionUserPerformsAction(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean evaluate(WiredContext ctx) { + List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); + if (targets.isEmpty()) { + return false; + } + + if (this.quantifier == QUANTIFIER_ANY) { + return targets.stream().anyMatch(roomUnit -> this.matchesAction(ctx, roomUnit)); + } + + return targets.stream().allMatch(roomUnit -> this.matchesAction(ctx, roomUnit)); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.selectedAction, + this.signFilterEnabled, + this.signId, + this.danceFilterEnabled, + this.danceId, + this.userSource, + this.quantifier + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.resetSettings(); + + String wiredData = set.getString("wired_data"); + + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data == null) { + return; + } + + this.selectedAction = normalizeAction(data.selectedAction); + this.signFilterEnabled = data.signFilterEnabled; + this.signId = normalizeSignId(data.signId); + this.danceFilterEnabled = data.danceFilterEnabled; + this.danceId = normalizeDanceId(data.danceId); + this.userSource = this.normalizeUserSource(data.userSource); + this.quantifier = normalizeQuantifier(data.quantifier); + } + + @Override + public void onPickUp() { + this.resetSettings(); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(true); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(7); + message.appendInt(this.selectedAction); + message.appendInt(this.signFilterEnabled ? 1 : 0); + message.appendInt(this.signId); + message.appendInt(this.danceFilterEnabled ? 1 : 0); + message.appendInt(this.danceId); + message.appendInt(this.userSource); + message.appendInt(this.quantifier); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] intParams = settings.getIntParams(); + + this.resetSettings(); + + if (intParams.length > 0) this.selectedAction = normalizeAction(intParams[0]); + if (intParams.length > 1) this.signFilterEnabled = (intParams[1] == 1); + if (intParams.length > 2) this.signId = normalizeSignId(intParams[2]); + if (intParams.length > 3) this.danceFilterEnabled = (intParams[3] == 1); + if (intParams.length > 4) this.danceId = normalizeDanceId(intParams[4]); + if (intParams.length > 5) this.userSource = this.normalizeUserSource(intParams[5]); + if (intParams.length > 6) this.quantifier = normalizeQuantifier(intParams[6]); + + return true; + } + + protected void resetSettings() { + this.selectedAction = DEFAULT_ACTION; + this.signFilterEnabled = false; + this.signId = 0; + this.danceFilterEnabled = false; + this.danceId = 1; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; + } + + protected int normalizeAction(int action) { + switch (action) { + case WiredUserActionType.WAVE: + case WiredUserActionType.BLOW_KISS: + case WiredUserActionType.LAUGH: + case WiredUserActionType.AWAKE: + case WiredUserActionType.RELAX: + case WiredUserActionType.SIT: + case WiredUserActionType.STAND: + case WiredUserActionType.LAY: + case WiredUserActionType.SIGN: + case WiredUserActionType.DANCE: + case WiredUserActionType.THUMB_UP: + return action; + default: + return DEFAULT_ACTION; + } + } + + protected int normalizeQuantifier(int value) { + return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; + } + + protected int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + protected int normalizeSignId(int value) { + return (value < 0 || value > 17) ? 0 : value; + } + + protected int normalizeDanceId(int value) { + return (value < 1 || value > 4) ? 1 : value; + } + + protected boolean matchesAction(WiredContext ctx, RoomUnit roomUnit) { + if (roomUnit == null) { + return false; + } + + if (this.matchesEventAction(ctx, roomUnit)) { + return true; + } + + if (this.matchesCurrentState(roomUnit)) { + return true; + } + + return this.matchesRecentAction(roomUnit); + } + + protected boolean matchesEventAction(WiredContext ctx, RoomUnit roomUnit) { + RoomUnit actor = ctx.actor().orElse(null); + + if (actor == null || actor.getId() != roomUnit.getId()) { + return false; + } + + if (ctx.eventType() != com.eu.habbo.habbohotel.wired.core.WiredEvent.Type.USER_PERFORMS_ACTION) { + return false; + } + + return this.matchesConfiguredAction(ctx.event().getActionId(), ctx.event().getActionParameter()); + } + + protected boolean matchesCurrentState(RoomUnit roomUnit) { + switch (this.selectedAction) { + case WiredUserActionType.SIT: + return roomUnit.hasStatus(RoomUnitStatus.SIT); + case WiredUserActionType.LAY: + return roomUnit.hasStatus(RoomUnitStatus.LAY); + case WiredUserActionType.RELAX: + return roomUnit.isIdle(); + case WiredUserActionType.SIGN: + return this.matchesSignState(roomUnit); + case WiredUserActionType.DANCE: + return this.matchesDanceState(roomUnit); + default: + return false; + } + } + + protected boolean matchesRecentAction(RoomUnit roomUnit) { + Object actionValue = roomUnit.getCacheable().get(CACHE_LAST_ACTION_ID); + Object parameterValue = roomUnit.getCacheable().get(CACHE_LAST_ACTION_PARAMETER); + Object timestampValue = roomUnit.getCacheable().get(CACHE_LAST_ACTION_TIMESTAMP); + + if (!(actionValue instanceof Integer) || !(timestampValue instanceof Long)) { + return false; + } + + long timestamp = (Long) timestampValue; + if ((System.currentTimeMillis() - timestamp) > TRANSIENT_ACTION_WINDOW_MS) { + return false; + } + + int actionId = (Integer) actionValue; + int parameter = (parameterValue instanceof Integer) ? (Integer) parameterValue : -1; + + return this.matchesConfiguredAction(actionId, parameter); + } + + protected boolean matchesConfiguredAction(int actionId, int actionParameter) { + if (actionId != this.selectedAction) { + return false; + } + + if (this.selectedAction == WiredUserActionType.SIGN && this.signFilterEnabled) { + return actionParameter == this.signId; + } + + if (this.selectedAction == WiredUserActionType.DANCE && this.danceFilterEnabled) { + return actionParameter == this.danceId; + } + + return true; + } + + protected boolean matchesSignState(RoomUnit roomUnit) { + String signStatus = roomUnit.getStatus(RoomUnitStatus.SIGN); + if (signStatus == null) { + return false; + } + + if (!this.signFilterEnabled) { + return true; + } + + try { + return Integer.parseInt(signStatus) == this.signId; + } catch (NumberFormatException ignored) { + return false; + } + } + + protected boolean matchesDanceState(RoomUnit roomUnit) { + int currentDance = roomUnit.getDanceType().getType(); + if (currentDance <= 0) { + return false; + } + + if (!this.danceFilterEnabled) { + return true; + } + + return currentDance == this.danceId; + } + + protected int getUserSource() { + return this.userSource; + } + + protected int getQuantifier() { + return this.quantifier; + } + + static class JsonData { + int selectedAction; + boolean signFilterEnabled; + int signId; + boolean danceFilterEnabled; + int danceId; + int userSource; + int quantifier; + + public JsonData(int selectedAction, boolean signFilterEnabled, int signId, boolean danceFilterEnabled, int danceId, int userSource, int quantifier) { + this.selectedAction = selectedAction; + this.signFilterEnabled = signFilterEnabled; + this.signId = signId; + this.danceFilterEnabled = danceFilterEnabled; + this.danceId = danceId; + this.userSource = userSource; + this.quantifier = quantifier; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableAgeMatch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableAgeMatch.java new file mode 100644 index 00000000..18456674 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableAgeMatch.java @@ -0,0 +1,420 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable { + public static final WiredConditionType type = WiredConditionType.VAR_AGE_MATCH; + + private static final int TARGET_CONTEXT = 2; + private static final int COMPARE_VALUE_CREATED = 0; + private static final int COMPARE_VALUE_UPDATED = 1; + private static final int COMPARISON_LOWER_THAN = 0; + private static final int COMPARISON_HIGHER_THAN = 2; + private static final int DURATION_UNIT_MILLISECONDS = 0; + private static final int DURATION_UNIT_SECONDS = 1; + private static final int DURATION_UNIT_MINUTES = 2; + private static final int DURATION_UNIT_HOURS = 3; + private static final int DURATION_UNIT_DAYS = 4; + private static final int DURATION_UNIT_WEEKS = 5; + private static final int DURATION_UNIT_MONTHS = 6; + private static final int DURATION_UNIT_YEARS = 7; + + protected int compareValue = COMPARE_VALUE_CREATED; + protected int comparison = COMPARISON_LOWER_THAN; + protected int durationAmount = 0; + protected int durationUnit = DURATION_UNIT_SECONDS; + + public WiredConditionVariableAgeMatch(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionVariableAgeMatch(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(); + + List serializedItems = new ArrayList<>(); + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + serializedItems.addAll(this.selectedItems); + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(serializedItems.size()); + + for (HabboItem item : serializedItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableToken == null ? "" : this.variableToken); + message.appendInt(8); + message.appendInt(this.targetType); + message.appendInt(this.compareValue); + message.appendInt(this.comparison); + message.appendInt(this.durationAmount); + message.appendInt(this.durationUnit); + message.appendInt(this.userSource); + message.appendInt(this.furniSource); + message.appendInt(this.quantifier); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] params = settings.getIntParams(); + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + this.targetType = (params.length > 0) ? normalizeTargetTypeExtended(params[0]) : TARGET_USER; + this.compareValue = (params.length > 1) ? normalizeCompareValue(params[1]) : COMPARE_VALUE_CREATED; + this.comparison = (params.length > 2) ? normalizeComparison(params[2]) : COMPARISON_LOWER_THAN; + this.durationAmount = Math.max(0, (params.length > 3) ? params[3] : 0); + this.durationUnit = (params.length > 4) ? normalizeDurationUnit(params[4]) : DURATION_UNIT_SECONDS; + this.userSource = (params.length > 5) ? normalizeUserSource(params[5]) : WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = (params.length > 6) ? normalizeFurniSource(params[6]) : WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = (params.length > 7) ? normalizeQuantifier(params[7]) : QUANTIFIER_ALL; + this.setVariableToken(normalizeVariableToken(settings.getStringParam())); + + if (!this.isValidSource(room)) { + return false; + } + + this.selectedItems.clear(); + + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED && room != null) { + int[] furniIds = settings.getFurniIds(); + if (furniIds.length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + return false; + } + + for (int furniId : furniIds) { + HabboItem item = room.getHabboItem(furniId); + + if (item != null) { + this.selectedItems.add(item); + } + } + } + + return true; + } + + @Override + public boolean evaluate(WiredContext ctx) { + Room room = ctx.room(); + + if (room == null || this.variableToken == null || this.variableToken.isEmpty() || !isCustomVariableToken(this.variableToken)) { + return false; + } + + long thresholdMs = durationToMillis(this.durationAmount, this.durationUnit); + + return switch (this.targetType) { + case TARGET_FURNI -> this.evaluateFurniTargets(ctx, room, thresholdMs); + case TARGET_ROOM -> this.evaluateRoomTarget(room, thresholdMs); + case TARGET_CONTEXT -> this.evaluateContextTarget(ctx, room, thresholdMs); + default -> this.evaluateUserTargets(ctx, room, thresholdMs); + }; + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + this.refresh(); + + List itemIds = new ArrayList<>(); + for (HabboItem item : this.selectedItems) { + if (item != null) itemIds.add(item.getId()); + } + + return WiredManager.getGson().toJson(new JsonData( + itemIds, + this.targetType, + this.variableToken, + this.variableItemId, + this.compareValue, + this.comparison, + this.durationAmount, + this.durationUnit, + this.userSource, + this.furniSource, + this.quantifier + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) return; + + try { + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data == null) return; + + this.targetType = normalizeTargetTypeExtended(data.targetType); + this.compareValue = normalizeCompareValue(data.compareValue); + this.comparison = normalizeComparison(data.comparison); + this.durationAmount = Math.max(0, data.durationAmount); + this.durationUnit = normalizeDurationUnit(data.durationUnit); + this.userSource = normalizeUserSource(data.userSource); + this.furniSource = normalizeFurniSource(data.furniSource); + this.quantifier = normalizeQuantifier(data.quantifier); + this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); + + if (room != null && data.itemIds != null) { + for (Integer itemId : data.itemIds) { + if (itemId == null || itemId <= 0) continue; + + HabboItem item = room.getHabboItem(itemId); + if (item != null) this.selectedItems.add(item); + } + } + + return; + } + + this.setVariableToken(normalizeVariableToken(wiredData)); + } catch (Exception e) { + this.onPickUp(); + } + } + + @Override + public void onPickUp() { + super.onPickUp(); + this.compareValue = COMPARE_VALUE_CREATED; + this.comparison = COMPARISON_LOWER_THAN; + this.durationAmount = 0; + this.durationUnit = DURATION_UNIT_SECONDS; + } + + private boolean evaluateUserTargets(WiredContext ctx, Room room, long thresholdMs) { + List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); + if (targets.isEmpty()) return false; + + if (this.quantifier == QUANTIFIER_ANY) { + for (RoomUnit roomUnit : targets) { + if (this.matchesAge(this.readUserAgeMs(room, roomUnit), thresholdMs)) return true; + } + + return false; + } + + for (RoomUnit roomUnit : targets) { + if (!this.matchesAge(this.readUserAgeMs(room, roomUnit), thresholdMs)) return false; + } + + return true; + } + + private boolean evaluateFurniTargets(WiredContext ctx, Room room, long thresholdMs) { + this.refresh(); + + List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.selectedItems); + if (targets.isEmpty()) return false; + + if (this.quantifier == QUANTIFIER_ANY) { + for (HabboItem item : targets) { + if (this.matchesAge(this.readFurniAgeMs(room, item), thresholdMs)) return true; + } + + return false; + } + + for (HabboItem item : targets) { + if (!this.matchesAge(this.readFurniAgeMs(room, item), thresholdMs)) return false; + } + + return true; + } + + private boolean evaluateRoomTarget(Room room, long thresholdMs) { + return this.matchesAge(this.readRoomAgeMs(room), thresholdMs); + } + + private boolean evaluateContextTarget(WiredContext ctx, Room room, long thresholdMs) { + return this.matchesAge(this.readContextAgeMs(ctx, room), thresholdMs); + } + + private Long readUserAgeMs(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) return null; + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo == null || !room.getUserVariableManager().hasVariable(habbo.getHabboInfo().getId(), this.variableItemId)) return null; + + int timestamp = (this.compareValue == COMPARE_VALUE_UPDATED) + ? room.getUserVariableManager().getUpdatedAt(habbo.getHabboInfo().getId(), this.variableItemId) + : room.getUserVariableManager().getCreatedAt(habbo.getHabboInfo().getId(), this.variableItemId); + + return timestampToAgeMs(timestamp); + } + + private Long readFurniAgeMs(Room room, HabboItem item) { + if (room == null || item == null || !room.getFurniVariableManager().hasVariable(item.getId(), this.variableItemId)) return null; + + int timestamp = (this.compareValue == COMPARE_VALUE_UPDATED) + ? room.getFurniVariableManager().getUpdatedAt(item.getId(), this.variableItemId) + : room.getFurniVariableManager().getCreatedAt(item.getId(), this.variableItemId); + + return timestampToAgeMs(timestamp); + } + + private Long readRoomAgeMs(Room room) { + if (room == null) return null; + if (this.compareValue == COMPARE_VALUE_CREATED) return null; + + int timestamp = room.getRoomVariableManager().getUpdatedAt(this.variableItemId); + return timestampToAgeMs(timestamp); + } + + private Long readContextAgeMs(WiredContext ctx, Room room) { + if (ctx == null || room == null || !WiredContextVariableSupport.hasVariable(ctx, this.variableItemId)) return null; + + int timestamp = (this.compareValue == COMPARE_VALUE_UPDATED) + ? WiredContextVariableSupport.getUpdatedAt(ctx, this.variableItemId) + : WiredContextVariableSupport.getCreatedAt(ctx, this.variableItemId); + + return timestampToAgeMs(timestamp); + } + + private boolean matchesAge(Long ageMs, long thresholdMs) { + if (ageMs == null) return false; + + return switch (this.comparison) { + case COMPARISON_HIGHER_THAN -> ageMs > thresholdMs; + default -> ageMs < thresholdMs; + }; + } + + private boolean isValidSource(Room room) { + if (room == null || !isCustomVariableToken(this.variableToken)) return false; + + return switch (this.targetType) { + case TARGET_FURNI -> room.getFurniVariableManager().getDefinitionInfo(this.variableItemId) != null; + case TARGET_CONTEXT -> WiredContextVariableSupport.getDefinitionInfo(room, this.variableItemId) != null; + case TARGET_ROOM -> { + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(this.variableItemId); + yield this.compareValue == COMPARE_VALUE_UPDATED && definition != null; + } + default -> room.getUserVariableManager().getDefinitionInfo(this.variableItemId) != null; + }; + } + + private static Long timestampToAgeMs(int timestampSeconds) { + if (timestampSeconds <= 0) return null; + + long timestampMs = (timestampSeconds * 1000L); + return Math.max(0L, System.currentTimeMillis() - timestampMs); + } + + private static long durationToMillis(int amount, int unit) { + long normalizedAmount = Math.max(0L, amount); + + return switch (unit) { + case DURATION_UNIT_MILLISECONDS -> normalizedAmount; + case DURATION_UNIT_MINUTES -> safeMultiply(normalizedAmount, 60_000L); + case DURATION_UNIT_HOURS -> safeMultiply(normalizedAmount, 3_600_000L); + case DURATION_UNIT_DAYS -> safeMultiply(normalizedAmount, 86_400_000L); + case DURATION_UNIT_WEEKS -> safeMultiply(normalizedAmount, 604_800_000L); + case DURATION_UNIT_MONTHS -> safeMultiply(normalizedAmount, 2_592_000_000L); + case DURATION_UNIT_YEARS -> safeMultiply(normalizedAmount, 31_536_000_000L); + default -> safeMultiply(normalizedAmount, 1_000L); + }; + } + + private static long safeMultiply(long left, long right) { + if (left <= 0 || right <= 0) return 0L; + if (left > (Long.MAX_VALUE / right)) return Long.MAX_VALUE; + + return left * right; + } + + private static int normalizeTargetTypeExtended(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + private static int normalizeCompareValue(int value) { + return (value == COMPARE_VALUE_UPDATED) ? COMPARE_VALUE_UPDATED : COMPARE_VALUE_CREATED; + } + + private static int normalizeComparison(int value) { + return (value == COMPARISON_HIGHER_THAN) ? COMPARISON_HIGHER_THAN : COMPARISON_LOWER_THAN; + } + + private static int normalizeDurationUnit(int value) { + return switch (value) { + case DURATION_UNIT_MILLISECONDS, DURATION_UNIT_SECONDS, DURATION_UNIT_MINUTES, DURATION_UNIT_HOURS, + DURATION_UNIT_DAYS, DURATION_UNIT_WEEKS, DURATION_UNIT_MONTHS, DURATION_UNIT_YEARS -> value; + default -> DURATION_UNIT_SECONDS; + }; + } + + protected static class JsonData { + List itemIds; + int targetType; + String variableToken; + int variableItemId; + int compareValue; + int comparison; + int durationAmount; + int durationUnit; + int userSource; + int furniSource; + int quantifier; + + JsonData(List itemIds, int targetType, String variableToken, int variableItemId, int compareValue, int comparison, int durationAmount, int durationUnit, int userSource, int furniSource, int quantifier) { + this.itemIds = itemIds; + this.targetType = targetType; + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.compareValue = compareValue; + this.comparison = comparison; + this.durationAmount = durationAmount; + this.durationUnit = durationUnit; + this.userSource = userSource; + this.furniSource = furniSource; + this.quantifier = quantifier; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableValueMatch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableValueMatch.java new file mode 100644 index 00000000..13f79233 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableValueMatch.java @@ -0,0 +1,814 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame; +import com.eu.habbo.habbohotel.games.freeze.FreezeGame; +import com.eu.habbo.habbohotel.games.wired.WiredGame; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredConditionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredInternalVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.util.HotelDateTimeUtil; +import gnu.trove.set.hash.THashSet; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; + +public class WiredConditionVariableValueMatch extends WiredConditionHasVariable { + public static final WiredConditionType type = WiredConditionType.VAR_VAL_MATCH; + + private static final int TARGET_CONTEXT = 2; + private static final int SOURCE_SECONDARY_SELECTED = 101; + private static final int REFERENCE_CONSTANT = 0; + private static final int REFERENCE_VARIABLE = 1; + private static final int COMPARISON_GREATER_THAN = 0; + private static final int COMPARISON_GREATER_THAN_OR_EQUAL = 1; + private static final int COMPARISON_EQUAL = 2; + private static final int COMPARISON_LESS_THAN_OR_EQUAL = 3; + private static final int COMPARISON_LESS_THAN = 4; + private static final int COMPARISON_NOT_EQUAL = 5; + private static final String DELIM = "\t"; + private static final String FURNI_DELIM = ";"; + + protected int comparison = COMPARISON_EQUAL; + protected int referenceMode = REFERENCE_CONSTANT; + protected int referenceConstantValue = 0; + protected int referenceTargetType = TARGET_USER; + protected int referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + protected String referenceVariableToken = ""; + protected int referenceVariableItemId = 0; + protected final THashSet referenceSelectedItems = new THashSet<>(); + + public WiredConditionVariableValueMatch(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredConditionVariableValueMatch(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public WiredConditionType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(); + this.refreshReferenceItems(); + + List serializedItems = new ArrayList<>(); + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + serializedItems.addAll(this.selectedItems); + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(serializedItems.size()); + + for (HabboItem item : serializedItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.serializeStringData()); + message.appendInt(10); + message.appendInt(this.targetType); + message.appendInt(this.comparison); + message.appendInt(this.referenceMode); + message.appendInt(this.referenceConstantValue); + message.appendInt(this.referenceTargetType); + message.appendInt(this.userSource); + message.appendInt(this.furniSource); + message.appendInt(this.referenceUserSource); + message.appendInt(this.referenceFurniSource); + message.appendInt(this.quantifier); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) return false; + + int[] params = settings.getIntParams(); + String[] stringParts = this.parseStringData(settings.getStringParam()); + int nextTargetType = normalizeTargetTypeExtended(param(params, 0, TARGET_USER)); + int nextComparison = normalizeComparison(param(params, 1, COMPARISON_EQUAL)); + int nextReferenceMode = normalizeReferenceMode(param(params, 2, REFERENCE_CONSTANT)); + int nextReferenceConstantValue = param(params, 3, 0); + int nextReferenceTargetType = normalizeTargetTypeExtended(param(params, 4, TARGET_USER)); + int nextUserSource = normalizeUserSource(param(params, 5, WiredSourceUtil.SOURCE_TRIGGER)); + int nextFurniSource = normalizeFurniSource(param(params, 6, WiredSourceUtil.SOURCE_TRIGGER)); + int nextReferenceUserSource = normalizeUserSource(param(params, 7, WiredSourceUtil.SOURCE_TRIGGER)); + int nextReferenceFurniSource = normalizeReferenceFurniSource(param(params, 8, WiredSourceUtil.SOURCE_TRIGGER)); + int nextQuantifier = normalizeQuantifier(param(params, 9, QUANTIFIER_ALL)); + String nextVariableToken = normalizeVariableToken((stringParts.length > 0) ? stringParts[0] : settings.getStringParam()); + String nextReferenceVariableToken = normalizeVariableToken((stringParts.length > 1) ? stringParts[1] : ""); + + if (!this.isValidSource(room, nextTargetType, nextVariableToken)) return false; + if (nextReferenceMode == REFERENCE_VARIABLE && !this.isValidReference(room, nextReferenceTargetType, nextReferenceVariableToken)) return false; + + int selectionLimit = Emulator.getConfig().getInt("hotel.wired.furni.selection.count"); + List nextSelectedItems = (nextTargetType == TARGET_FURNI && nextFurniSource == WiredSourceUtil.SOURCE_SELECTED) + ? this.parseItems(settings.getFurniIds(), room) + : new ArrayList<>(); + List nextReferenceItems = (nextReferenceMode == REFERENCE_VARIABLE && nextReferenceTargetType == TARGET_FURNI && nextReferenceFurniSource == SOURCE_SECONDARY_SELECTED) + ? this.parseItems((stringParts.length > 2) ? stringParts[2] : "", room) + : new ArrayList<>(); + + if (nextSelectedItems.size() > selectionLimit || nextReferenceItems.size() > selectionLimit) return false; + + this.selectedItems.clear(); + this.selectedItems.addAll(nextSelectedItems); + this.referenceSelectedItems.clear(); + this.referenceSelectedItems.addAll(nextReferenceItems); + this.targetType = nextTargetType; + this.comparison = nextComparison; + this.referenceMode = nextReferenceMode; + this.referenceConstantValue = nextReferenceConstantValue; + this.referenceTargetType = nextReferenceTargetType; + this.userSource = nextUserSource; + this.furniSource = nextFurniSource; + this.referenceUserSource = nextReferenceUserSource; + this.referenceFurniSource = nextReferenceFurniSource; + this.quantifier = nextQuantifier; + this.setVariableToken(nextVariableToken); + this.setReferenceVariableToken(nextReferenceVariableToken); + + return true; + } + + @Override + public boolean evaluate(WiredContext ctx) { + Room room = ctx.room(); + + if (room == null || this.variableToken == null || this.variableToken.isEmpty()) { + return false; + } + + return switch (this.targetType) { + case TARGET_FURNI -> this.evaluateFurniTargets(ctx, room); + case TARGET_ROOM -> this.evaluateRoomTarget(ctx, room); + case TARGET_CONTEXT -> this.evaluateContextTarget(ctx, room); + default -> this.evaluateUserTargets(ctx, room); + }; + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + this.refresh(); + this.refreshReferenceItems(); + + return WiredManager.getGson().toJson(new JsonData( + this.targetType, + this.variableToken, + this.variableItemId, + this.comparison, + this.referenceMode, + this.referenceConstantValue, + this.referenceTargetType, + this.referenceVariableToken, + this.referenceVariableItemId, + this.userSource, + this.furniSource, + this.referenceUserSource, + this.referenceFurniSource, + this.quantifier, + this.toIds(this.selectedItems), + this.toIds(this.referenceSelectedItems) + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) return; + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) return; + + this.targetType = normalizeTargetTypeExtended(data.targetType); + this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); + this.comparison = normalizeComparison(data.comparison); + this.referenceMode = normalizeReferenceMode(data.referenceMode); + this.referenceConstantValue = data.referenceConstantValue; + this.referenceTargetType = normalizeTargetTypeExtended(data.referenceTargetType); + this.setReferenceVariableToken(normalizeVariableToken((data.referenceVariableToken != null) ? data.referenceVariableToken : ((data.referenceVariableItemId > 0) ? String.valueOf(data.referenceVariableItemId) : ""))); + this.userSource = normalizeUserSource(data.userSource); + this.furniSource = normalizeFurniSource(data.furniSource); + this.referenceUserSource = normalizeUserSource(data.referenceUserSource); + this.referenceFurniSource = normalizeReferenceFurniSource(data.referenceFurniSource); + this.quantifier = normalizeQuantifier(data.quantifier); + + if (room == null) return; + + this.selectedItems.addAll(this.parseItems(data.selectedItemIds, room)); + this.referenceSelectedItems.addAll(this.parseItems(data.referenceSelectedItemIds, room)); + } + + @Override + public void onPickUp() { + super.onPickUp(); + this.comparison = COMPARISON_EQUAL; + this.referenceMode = REFERENCE_CONSTANT; + this.referenceConstantValue = 0; + this.referenceTargetType = TARGET_USER; + this.referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER; + this.referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.referenceSelectedItems.clear(); + this.setReferenceVariableToken(""); + } + + public boolean requiresTriggeringUser() { + return (this.targetType == TARGET_USER && this.userSource == WiredSourceUtil.SOURCE_TRIGGER) + || (this.referenceMode == REFERENCE_VARIABLE && this.referenceTargetType == TARGET_USER && this.referenceUserSource == WiredSourceUtil.SOURCE_TRIGGER); + } + + private boolean evaluateUserTargets(WiredContext ctx, Room room) { + List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); + if (targets.isEmpty()) return false; + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + + if (this.quantifier == QUANTIFIER_ANY) { + int index = 0; + for (RoomUnit roomUnit : targets) { + Integer currentValue = this.readUserValue(room, roomUnit); + Integer referenceValue = this.referenceFor(references, roomUnit != null ? roomUnit.getId() : 0, TARGET_USER, index++); + + if (this.matchesComparison(currentValue, referenceValue)) return true; + } + + return false; + } + + int index = 0; + for (RoomUnit roomUnit : targets) { + Integer currentValue = this.readUserValue(room, roomUnit); + Integer referenceValue = this.referenceFor(references, roomUnit != null ? roomUnit.getId() : 0, TARGET_USER, index++); + + if (!this.matchesComparison(currentValue, referenceValue)) return false; + } + + return true; + } + + private boolean evaluateFurniTargets(WiredContext ctx, Room room) { + this.refresh(); + + List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.selectedItems); + if (targets.isEmpty()) return false; + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + + if (this.quantifier == QUANTIFIER_ANY) { + int index = 0; + for (HabboItem item : targets) { + Integer currentValue = this.readFurniValue(room, item); + Integer referenceValue = this.referenceFor(references, item != null ? item.getId() : 0, TARGET_FURNI, index++); + + if (this.matchesComparison(currentValue, referenceValue)) return true; + } + + return false; + } + + int index = 0; + for (HabboItem item : targets) { + Integer currentValue = this.readFurniValue(room, item); + Integer referenceValue = this.referenceFor(references, item != null ? item.getId() : 0, TARGET_FURNI, index++); + + if (!this.matchesComparison(currentValue, referenceValue)) return false; + } + + return true; + } + + private boolean evaluateRoomTarget(WiredContext ctx, Room room) { + Integer currentValue = this.readRoomValue(room); + Integer referenceValue = this.referenceFor(this.resolveReferences(ctx, room), room.getId(), TARGET_ROOM, 0); + + return this.matchesComparison(currentValue, referenceValue); + } + + private boolean evaluateContextTarget(WiredContext ctx, Room room) { + Integer currentValue = this.readContextTargetValue(ctx, room); + Integer referenceValue = this.referenceFor(this.resolveReferences(ctx, room), this.variableItemId, TARGET_CONTEXT, 0); + + return this.matchesComparison(currentValue, referenceValue); + } + + private ReferenceSnapshot resolveReferences(WiredContext ctx, Room room) { + if (this.referenceMode != REFERENCE_VARIABLE) return null; + + return switch (this.referenceTargetType) { + case TARGET_USER -> this.userReferences(ctx, room); + case TARGET_FURNI -> this.furniReferences(ctx, room); + case TARGET_CONTEXT -> this.contextReferences(ctx, room); + case TARGET_ROOM -> this.roomReferences(room); + default -> null; + }; + } + + private ReferenceSnapshot userReferences(WiredContext ctx, Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_USER); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseUserInternalReference(key)) return null; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.referenceUserSource)) { + Integer value = this.readUserInternalValue(room, roomUnit, key); + if (value != null && roomUnit != null) snapshot.add(roomUnit.getId(), value); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.referenceUserSource)) { + if (roomUnit == null) continue; + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo != null) snapshot.add(roomUnit.getId(), room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.referenceVariableItemId)); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + private ReferenceSnapshot furniReferences(WiredContext ctx, Room room) { + int source = (this.referenceFurniSource == SOURCE_SECONDARY_SELECTED) ? WiredSourceUtil.SOURCE_SELECTED : this.referenceFurniSource; + if (source == WiredSourceUtil.SOURCE_SELECTED) this.refreshReferenceItems(); + + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_FURNI); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseFurniInternalReference(key)) return null; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, source, this.referenceSelectedItems)) { + Integer value = this.readFurniInternalValue(room, item, key); + if (value != null && item != null) snapshot.add(item.getId(), value); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, source, this.referenceSelectedItems)) { + if (item != null) snapshot.add(item.getId(), room.getFurniVariableManager().getCurrentValue(item.getId(), this.referenceVariableItemId)); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + private ReferenceSnapshot roomReferences(Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_ROOM); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseRoomInternalReference(key)) return null; + + Integer value = this.readRoomInternalValue(room, key); + if (value == null) return null; + + snapshot.add(room.getId(), value); + return snapshot; + } + + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + snapshot.add(room.getId(), room.getRoomVariableManager().getCurrentValue(this.referenceVariableItemId)); + return snapshot; + } + + private ReferenceSnapshot contextReferences(WiredContext ctx, Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_CONTEXT); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseContextInternalReference(key)) return null; + + Integer value = WiredInternalVariableSupport.readContextValue(ctx, key); + if (value == null) return null; + + snapshot.add(this.referenceVariableItemId > 0 ? this.referenceVariableItemId : (room != null ? room.getId() : 0), value); + return snapshot; + } + + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, this.referenceVariableItemId); + if (definition == null || !definition.hasValue() || !WiredContextVariableSupport.hasVariable(ctx, this.referenceVariableItemId)) return null; + + Integer value = WiredContextVariableSupport.getCurrentValue(ctx, this.referenceVariableItemId); + if (value == null) return null; + + snapshot.add(this.referenceVariableItemId, value); + return snapshot; + } + + private Integer readUserValue(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + return canUseUserInternalReference(key) ? this.readUserInternalValue(room, roomUnit, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.variableItemId); + if (definition == null || !definition.hasValue()) return null; + + Habbo habbo = room.getHabbo(roomUnit); + return (habbo != null) ? room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.variableItemId) : null; + } + + private Integer readFurniValue(Room room, HabboItem item) { + if (room == null || item == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + return canUseFurniInternalReference(key) ? this.readFurniInternalValue(room, item, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.variableItemId); + return (definition != null && definition.hasValue()) ? room.getFurniVariableManager().getCurrentValue(item.getId(), this.variableItemId) : null; + } + + private Integer readRoomValue(Room room) { + if (room == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + return canUseRoomInternalReference(key) ? this.readRoomInternalValue(room, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(this.variableItemId); + return (definition != null && definition.hasValue()) ? room.getRoomVariableManager().getCurrentValue(this.variableItemId) : null; + } + + private Integer readContextTargetValue(WiredContext ctx, Room room) { + if (ctx == null || room == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + return canUseContextInternalReference(key) ? WiredInternalVariableSupport.readContextValue(ctx, key) : null; + } + + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, this.variableItemId); + if (definition == null || !definition.hasValue() || !WiredContextVariableSupport.hasVariable(ctx, this.variableItemId)) return null; + + return WiredContextVariableSupport.getCurrentValue(ctx, this.variableItemId); + } + + private Integer referenceFor(ReferenceSnapshot snapshot, int destinationEntityId, int destinationTarget, int destinationIndex) { + if (this.referenceMode != REFERENCE_VARIABLE) return this.referenceConstantValue; + if (snapshot == null || snapshot.isEmpty()) return null; + if (snapshot.targetType == destinationTarget && snapshot.values.containsKey(destinationEntityId)) return snapshot.values.get(destinationEntityId); + if (destinationIndex >= 0 && destinationIndex < snapshot.values.size()) return new ArrayList<>(snapshot.values.values()).get(destinationIndex); + return new ArrayList<>(snapshot.values.values()).get(0); + } + + private boolean matchesComparison(Integer currentValue, Integer referenceValue) { + if (currentValue == null || referenceValue == null) return false; + + return switch (this.comparison) { + case COMPARISON_GREATER_THAN -> currentValue > referenceValue; + case COMPARISON_GREATER_THAN_OR_EQUAL -> currentValue >= referenceValue; + case COMPARISON_LESS_THAN_OR_EQUAL -> currentValue <= referenceValue; + case COMPARISON_LESS_THAN -> currentValue < referenceValue; + case COMPARISON_NOT_EQUAL -> !currentValue.equals(referenceValue); + default -> currentValue.equals(referenceValue); + }; + } + + private boolean isValidSource(Room room, int targetType, String variableToken) { + if (variableToken == null || variableToken.isEmpty()) return false; + + return switch (targetType) { + case TARGET_USER -> isInternalVariableToken(variableToken) + ? canUseUserInternalReference(getInternalVariableKey(variableToken)) + : this.isValidUserCustomValue(room, getCustomItemId(variableToken)); + case TARGET_FURNI -> isInternalVariableToken(variableToken) + ? canUseFurniInternalReference(getInternalVariableKey(variableToken)) + : this.isValidFurniCustomValue(room, getCustomItemId(variableToken)); + case TARGET_CONTEXT -> isInternalVariableToken(variableToken) + ? canUseContextInternalReference(getInternalVariableKey(variableToken)) + : this.isValidContextCustomValue(room, getCustomItemId(variableToken)); + case TARGET_ROOM -> isInternalVariableToken(variableToken) + ? canUseRoomInternalReference(getInternalVariableKey(variableToken)) + : this.isValidRoomCustomValue(room, getCustomItemId(variableToken)); + default -> false; + }; + } + + private boolean isValidReference(Room room, int targetType, String variableToken) { + return this.isValidSource(room, targetType, variableToken); + } + + private boolean isValidUserCustomValue(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getUserVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue(); + } + + private boolean isValidFurniCustomValue(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getFurniVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue(); + } + + private boolean isValidRoomCustomValue(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getRoomVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue(); + } + + private boolean isValidContextCustomValue(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, variableItemId); + return definition != null && definition.hasValue(); + } + + private Integer readUserInternalValue(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.readUserValue(room, roomUnit, key); + } + + private Integer readFurniInternalValue(Room room, HabboItem item, String key) { + return WiredInternalVariableSupport.readFurniValue(room, item, key); + } + + private Integer readRoomInternalValue(Room room, String key) { + return WiredInternalVariableSupport.readRoomValue(room, key); + } + + private Integer getUserTeamScore(Room room, Habbo habbo) { + if (room == null || habbo == null || habbo.getHabboInfo().getGamePlayer() == null) return null; + + Game game = this.resolveTeamGame(room, habbo); + GamePlayer gamePlayer = habbo.getHabboInfo().getGamePlayer(); + + if (game == null || gamePlayer.getTeamColor() == null) return gamePlayer.getScore(); + + GameTeam team = game.getTeam(gamePlayer.getTeamColor()); + return (team != null) ? team.getTotalScore() : gamePlayer.getScore(); + } + + private Integer getTeamColorId(int effectId) { + TeamEffectData data = this.getTeamEffectData(effectId); + return data == null ? null : data.colorId; + } + + private Integer getTeamTypeId(int effectId) { + TeamEffectData data = this.getTeamEffectData(effectId); + return data == null ? null : data.typeId; + } + + private int getTeamMetric(Room room, GameTeamColors color, boolean score) { + Game game = this.resolveTeamGame(room, null); + if (game == null || color == null) return 0; + + GameTeam team = game.getTeam(color); + if (team == null) return 0; + + return score ? team.getTotalScore() : team.getMembers().size(); + } + + private Game resolveTeamGame(Room room, Habbo habbo) { + if (room == null) return null; + + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getCurrentGame() != null) { + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (game != null) return game; + } + + Game wiredGame = room.getGame(WiredGame.class); + if (wiredGame != null) return wiredGame; + + Game freezeGame = room.getGame(FreezeGame.class); + if (freezeGame != null) return freezeGame; + + return room.getGame(BattleBanzaiGame.class); + } + + private List parseItems(int[] ids, Room room) { + List items = new ArrayList<>(); + if (ids == null || room == null) return items; + + for (int id : ids) { + HabboItem item = room.getHabboItem(id); + if (item != null) items.add(item); + } + + return items; + } + + private List parseItems(List ids, Room room) { + List items = new ArrayList<>(); + if (ids == null || room == null) return items; + + for (Integer id : ids) { + if (id == null || id <= 0) continue; + + HabboItem item = room.getHabboItem(id); + if (item != null) items.add(item); + } + + return items; + } + + private List parseItems(String ids, Room room) { + List items = new ArrayList<>(); + if (ids == null || ids.trim().isEmpty() || room == null) return items; + + for (String part : ids.split("[;,\\t]")) { + int id = parseInteger(part); + if (id <= 0) continue; + + HabboItem item = room.getHabboItem(id); + if (item != null) items.add(item); + } + + return items; + } + + private void refreshReferenceItems() { + THashSet staleItems = new THashSet<>(); + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + staleItems.addAll(this.referenceSelectedItems); + } else { + for (HabboItem item : this.referenceSelectedItems) { + if (item == null || item.getRoomId() != room.getId() || room.getHabboItem(item.getId()) == null) { + staleItems.add(item); + } + } + } + + this.referenceSelectedItems.removeAll(staleItems); + } + + private String serializeStringData() { + return (this.variableToken == null ? "" : this.variableToken) + DELIM + (this.referenceVariableToken == null ? "" : this.referenceVariableToken) + DELIM + this.serializeIds(this.referenceSelectedItems); + } + + private String[] parseStringData(String value) { + return (value == null || value.isEmpty()) ? new String[0] : value.split("\\t", -1); + } + + private List toIds(THashSet items) { + List ids = new ArrayList<>(); + for (HabboItem item : items) { + if (item != null) ids.add(item.getId()); + } + return ids; + } + + private String serializeIds(THashSet items) { + StringBuilder builder = new StringBuilder(); + + for (HabboItem item : items) { + if (item == null) continue; + if (builder.length() > 0) builder.append(FURNI_DELIM); + builder.append(item.getId()); + } + + return builder.toString(); + } + + private void setReferenceVariableToken(String token) { + this.referenceVariableToken = normalizeVariableToken(token); + this.referenceVariableItemId = getCustomItemId(this.referenceVariableToken); + } + + private static boolean canUseUserInternalReference(String key) { + return WiredInternalVariableSupport.canUseUserReference(key); + } + + private static boolean canUseFurniInternalReference(String key) { + return WiredInternalVariableSupport.canUseFurniReference(key); + } + + private static boolean canUseRoomInternalReference(String key) { + return WiredInternalVariableSupport.canUseRoomReference(key); + } + + private static boolean canUseContextInternalReference(String key) { + return WiredInternalVariableSupport.canUseContextReference(key); + } + + private static int param(int[] params, int index, int fallback) { + return (params.length > index) ? params[index] : fallback; + } + + private static int normalizeTargetTypeExtended(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + private static int normalizeReferenceMode(int value) { + return (value == REFERENCE_VARIABLE) ? REFERENCE_VARIABLE : REFERENCE_CONSTANT; + } + + private static int normalizeReferenceFurniSource(int value) { + return switch (value) { + case SOURCE_SECONDARY_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + } + + private static int normalizeComparison(int value) { + return switch (value) { + case COMPARISON_GREATER_THAN, COMPARISON_GREATER_THAN_OR_EQUAL, COMPARISON_LESS_THAN_OR_EQUAL, COMPARISON_LESS_THAN, COMPARISON_NOT_EQUAL -> value; + default -> COMPARISON_EQUAL; + }; + } + + private static int parseInteger(String value) { + try { + return (value == null || value.trim().isEmpty()) ? 0 : Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return 0; + } + } + + static class JsonData { + int targetType, variableItemId, comparison, referenceMode, referenceConstantValue, referenceTargetType, referenceVariableItemId, userSource, furniSource, referenceUserSource, referenceFurniSource, quantifier; + String variableToken, referenceVariableToken; + List selectedItemIds, referenceSelectedItemIds; + + JsonData(int targetType, String variableToken, int variableItemId, int comparison, int referenceMode, int referenceConstantValue, int referenceTargetType, String referenceVariableToken, int referenceVariableItemId, int userSource, int furniSource, int referenceUserSource, int referenceFurniSource, int quantifier, List selectedItemIds, List referenceSelectedItemIds) { + this.targetType = targetType; + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.comparison = comparison; + this.referenceMode = referenceMode; + this.referenceConstantValue = referenceConstantValue; + this.referenceTargetType = referenceTargetType; + this.referenceVariableToken = referenceVariableToken; + this.referenceVariableItemId = referenceVariableItemId; + this.userSource = userSource; + this.furniSource = furniSource; + this.referenceUserSource = referenceUserSource; + this.referenceFurniSource = referenceFurniSource; + this.quantifier = quantifier; + this.selectedItemIds = selectedItemIds; + this.referenceSelectedItemIds = referenceSelectedItemIds; + } + } + + private static class ReferenceSnapshot { + final int targetType; + final LinkedHashMap values = new LinkedHashMap<>(); + + ReferenceSnapshot(int targetType) { + this.targetType = targetType; + } + + void add(int entityId, int value) { + this.values.put(entityId, value); + } + + boolean isEmpty() { + return this.values.isEmpty(); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAdjustClock.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAdjustClock.java new file mode 100644 index 00000000..183eec7d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAdjustClock.java @@ -0,0 +1,252 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameUpCounter; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class WiredEffectAdjustClock extends InteractionWiredEffect { + private static final int OPERATOR_INCREASE = 0; + private static final int OPERATOR_DECREASE = 1; + private static final int OPERATOR_SET = 2; + private static final int MAX_MINUTES = 99; + private static final int MAX_HALF_SECOND_STEPS = 119; + + public static final WiredEffectType type = WiredEffectType.ADJUST_CLOCK; + + private final List items = new ArrayList<>(); + private int operator = OPERATOR_SET; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private int minutes = 0; + private int halfSecondSteps = 0; + + public WiredEffectAdjustClock(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectAdjustClock(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + + if (room == null) { + return; + } + + List effectiveItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + this.items.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + } + + for (HabboItem item : effectiveItems) { + if (!(item instanceof InteractionGameUpCounter)) { + continue; + } + + ((InteractionGameUpCounter) item).adjustCounter(room, this.operator, this.minutes, this.halfSecondSteps); + } + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.getDelay(), + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), + this.operator, + this.furniSource, + this.minutes, + this.halfSecondSteps + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.items.clear(); + String wiredData = set.getString("wired_data"); + + if (wiredData != null && wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.setDelay(data.delay); + this.operator = this.normalizeOperator(data.operator); + this.furniSource = data.furniSource; + this.minutes = this.normalizeMinutes(data.minutes); + this.halfSecondSteps = this.normalizeHalfSecondSteps(data.halfSecondSteps); + + if (data.itemIds != null) { + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + + if (item != null) { + this.items.add(item); + } + } + } + + return; + } + + this.operator = OPERATOR_SET; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.minutes = 0; + this.halfSecondSteps = 0; + this.setDelay(0); + } + + @Override + public void onPickUp() { + this.items.clear(); + this.operator = OPERATOR_SET; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.minutes = 0; + this.halfSecondSteps = 0; + this.setDelay(0); + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + List itemsSnapshot = new ArrayList<>(this.items); + itemsSnapshot.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + + this.items.clear(); + this.items.addAll(itemsSnapshot); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(itemsSnapshot.size()); + for (HabboItem item : itemsSnapshot) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(4); + message.appendInt(this.operator); + message.appendInt(this.furniSource); + message.appendInt(this.minutes); + message.appendInt(this.halfSecondSteps); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + int[] params = settings.getIntParams(); + + if (params.length < 4) { + throw new WiredSaveException("Invalid data"); + } + + this.operator = this.normalizeOperator(params[0]); + this.furniSource = params[1]; + this.minutes = this.normalizeMinutes(params[2]); + this.halfSecondSteps = this.normalizeHalfSecondSteps(params[3]); + + int delay = settings.getDelay(); + if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { + throw new WiredSaveException("Delay too long"); + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + + List newItems = new ArrayList<>(); + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item == null) { + throw new WiredSaveException(String.format("Item %s not found", itemId)); + } + + if (!(item instanceof InteractionGameUpCounter)) { + throw new WiredSaveException("wiredfurni.error.require_counter_furni"); + } + + newItems.add(item); + } + + this.items.clear(); + this.items.addAll(newItems); + this.setDelay(delay); + + return true; + } + + private int normalizeOperator(int value) { + if (value < OPERATOR_INCREASE || value > OPERATOR_SET) { + return OPERATOR_SET; + } + + return value; + } + + private int normalizeMinutes(int value) { + return Math.max(0, Math.min(MAX_MINUTES, value)); + } + + private int normalizeHalfSecondSteps(int value) { + return Math.max(0, Math.min(MAX_HALF_SECOND_STEPS, value)); + } + + static class JsonData { + int delay; + List itemIds; + int operator; + int furniSource; + int minutes; + int halfSecondSteps; + + public JsonData(int delay, List itemIds, int operator, int furniSource, int minutes, int halfSecondSteps) { + this.delay = delay; + this.itemIds = itemIds; + this.operator = operator; + this.furniSource = furniSource; + this.minutes = minutes; + this.halfSecondSteps = halfSecondSteps; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAlert.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAlert.java index ee4520c2..a6c9510e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAlert.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAlert.java @@ -3,11 +3,14 @@ package com.eu.habbo.habbohotel.items.interactions.wired.effects; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.Item; 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.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredTextPlaceholderUtil; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.List; public class WiredEffectAlert extends WiredEffectWhisper { public WiredEffectAlert(ResultSet set, Item baseItem) throws SQLException { @@ -21,15 +24,27 @@ public class WiredEffectAlert extends WiredEffectWhisper { @Override public void execute(WiredContext ctx) { Room room = ctx.room(); + List sourceUsers = resolveUsers(ctx); + List recipients = resolveRecipients(ctx, sourceUsers); + Habbo sharedSourceHabbo = (this.visibilitySelection == VISIBILITY_ALL_ROOM_USERS) + ? resolveMessageSourceHabbo(ctx, sourceUsers) + : null; - for (com.eu.habbo.habbohotel.rooms.RoomUnit unit : resolveUsers(ctx)) { - Habbo habbo = room.getHabbo(unit); - if (habbo == null) continue; + for (Habbo habbo : recipients) { + if (!shouldDeliverToRecipient(ctx, habbo)) { + continue; + } - habbo.alert(this.message + Habbo referenceHabbo = (sharedSourceHabbo != null) ? sharedSourceHabbo : habbo; + String username = (referenceHabbo != null && referenceHabbo.getHabboInfo() != null) + ? referenceHabbo.getHabboInfo().getUsername() + : ""; + + String message = this.message .replace("%online%", Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "") - .replace("%username%", habbo.getHabboInfo().getUsername()) - .replace("%roomsloaded%", Emulator.getGameEnvironment().getRoomManager().loadedRoomsCount() + "")); + .replace("%username%", username) + .replace("%roomsloaded%", Emulator.getGameEnvironment().getRoomManager().loadedRoomsCount() + ""); + habbo.alert(WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, message)); } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotClothes.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotClothes.java index 6cc5af2b..345fbb0c 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotClothes.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotClothes.java @@ -9,6 +9,7 @@ import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredBotSourceUtil; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.ServerMessage; @@ -24,6 +25,7 @@ public class WiredEffectBotClothes extends InteractionWiredEffect { private String botName = ""; private String botLook = ""; + private int botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; public WiredEffectBotClothes(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -41,7 +43,8 @@ public class WiredEffectBotClothes extends InteractionWiredEffect { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(this.botName + ((char) 9) + "" + this.botLook); - message.appendInt(0); + message.appendInt(1); + message.appendInt(this.botSource); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(this.getDelay()); @@ -65,6 +68,7 @@ public class WiredEffectBotClothes extends InteractionWiredEffect { if (data.length != 2) throw new WiredSaveException("Malformed data string. Invalid data length"); + this.botSource = (settings.getIntParams().length > 0) ? WiredBotSourceUtil.normalizeBotSource(settings.getIntParams()[0]) : WiredBotSourceUtil.SOURCE_BOT_NAME; this.botName = data[0].substring(0, Math.min(data[0].length(), Emulator.getConfig().getInt("hotel.wired.message.max_length", 100))); this.botLook = data[1]; this.setDelay(delay); @@ -80,10 +84,9 @@ public class WiredEffectBotClothes extends InteractionWiredEffect { @Override public void execute(WiredContext ctx) { Room room = ctx.room(); - List bots = room.getBots(this.botName); + List bots = WiredBotSourceUtil.resolveBots(ctx, room, this.botSource, this.botName); - if (bots.size() == 1) { - Bot bot = bots.get(0); + for (Bot bot : bots) { bot.setFigure(this.botLook); } } @@ -96,7 +99,7 @@ public class WiredEffectBotClothes extends InteractionWiredEffect { @Override public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData(this.botName, this.botLook, this.getDelay())); + return WiredManager.getGson().toJson(new JsonData(this.botName, this.botLook, this.getDelay(), this.botSource)); } @Override @@ -108,6 +111,9 @@ public class WiredEffectBotClothes extends InteractionWiredEffect { this.setDelay(data.delay); this.botName = data.bot_name; this.botLook = data.look; + this.botSource = (data.botSource != null) + ? WiredBotSourceUtil.normalizeBotSource(data.botSource) + : WiredBotSourceUtil.SOURCE_BOT_NAME; } else { String[] data = wiredData.split(((char) 9) + ""); @@ -119,6 +125,7 @@ public class WiredEffectBotClothes extends InteractionWiredEffect { } this.needsUpdate(true); + this.botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; } } @@ -126,9 +133,15 @@ public class WiredEffectBotClothes extends InteractionWiredEffect { public void onPickUp() { this.botLook = ""; this.botName = ""; + this.botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; this.setDelay(0); } + @Override + public boolean requiresTriggeringUser() { + return WiredBotSourceUtil.requiresTriggeringUser(this.botSource); + } + public String getBotName() { return this.botName; } @@ -149,11 +162,13 @@ public class WiredEffectBotClothes extends InteractionWiredEffect { String bot_name; String look; int delay; + Integer botSource; - public JsonData(String bot_name, String look, int delay) { + public JsonData(String bot_name, String look, int delay, int botSource) { this.bot_name = bot_name; this.look = look; this.delay = delay; + this.botSource = botSource; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotFollowHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotFollowHabbo.java index 6bdb028d..a1a80fe7 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotFollowHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotFollowHabbo.java @@ -11,6 +11,7 @@ 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.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredBotSourceUtil; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; @@ -29,6 +30,7 @@ public class WiredEffectBotFollowHabbo extends InteractionWiredEffect { private String botName = ""; private int mode = 0; private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; public WiredEffectBotFollowHabbo(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -46,9 +48,10 @@ public class WiredEffectBotFollowHabbo extends InteractionWiredEffect { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(this.botName); - message.appendInt(2); + message.appendInt(3); message.appendInt(this.mode); message.appendInt(this.userSource); + message.appendInt(this.botSource); message.appendInt(1); message.appendInt(this.getType().code); message.appendInt(this.getDelay()); @@ -79,6 +82,7 @@ public class WiredEffectBotFollowHabbo extends InteractionWiredEffect { int mode = settings.getIntParams()[0]; this.userSource = settings.getIntParams()[1]; + this.botSource = (settings.getIntParams().length > 2) ? WiredBotSourceUtil.normalizeBotSource(settings.getIntParams()[2]) : WiredBotSourceUtil.SOURCE_BOT_NAME; if(mode != 0 && mode != 1) throw new WiredSaveException("Mode is invalid"); @@ -111,15 +115,15 @@ public class WiredEffectBotFollowHabbo extends InteractionWiredEffect { RoomUnit roomUnit = targets.get(0); Habbo habbo = room.getHabbo(roomUnit); - List bots = room.getBots(this.botName); + List bots = WiredBotSourceUtil.resolveBots(ctx, room, this.botSource, this.botName); - if (habbo != null && bots.size() == 1) { - Bot bot = bots.get(0); - - if (this.mode == 1) { - bot.startFollowingHabbo(habbo); - } else { - bot.stopFollowingHabbo(); + if (habbo != null && !bots.isEmpty()) { + for (Bot bot : bots) { + if (this.mode == 1) { + bot.startFollowingHabbo(habbo); + } else { + bot.stopFollowingHabbo(); + } } } } @@ -132,7 +136,7 @@ public class WiredEffectBotFollowHabbo extends InteractionWiredEffect { @Override public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData(this.botName, this.mode, this.getDelay(), this.userSource)); + return WiredManager.getGson().toJson(new JsonData(this.botName, this.mode, this.getDelay(), this.userSource, this.botSource)); } @Override @@ -145,6 +149,9 @@ public class WiredEffectBotFollowHabbo extends InteractionWiredEffect { this.mode = data.mode; this.botName = data.bot_name; this.userSource = data.userSource; + this.botSource = (data.botSource != null) + ? WiredBotSourceUtil.normalizeBotSource(data.botSource) + : WiredBotSourceUtil.SOURCE_BOT_NAME; } else { String[] data = wiredData.split(((char) 9) + ""); @@ -157,6 +164,7 @@ public class WiredEffectBotFollowHabbo extends InteractionWiredEffect { this.needsUpdate(true); this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; } } @@ -165,12 +173,13 @@ public class WiredEffectBotFollowHabbo extends InteractionWiredEffect { this.botName = ""; this.mode = 0; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; this.setDelay(0); } @Override public boolean requiresTriggeringUser() { - return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; + return this.userSource == WiredSourceUtil.SOURCE_TRIGGER || WiredBotSourceUtil.requiresTriggeringUser(this.botSource); } static class JsonData { @@ -178,12 +187,14 @@ public class WiredEffectBotFollowHabbo extends InteractionWiredEffect { int mode; int delay; int userSource; + Integer botSource; - public JsonData(String bot_name, int mode, int delay, int userSource) { + public JsonData(String bot_name, int mode, int delay, int userSource, int botSource) { this.bot_name = bot_name; this.mode = mode; this.delay = delay; this.userSource = userSource; + this.botSource = botSource; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotGiveHandItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotGiveHandItem.java index c5350555..53a8be66 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotGiveHandItem.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotGiveHandItem.java @@ -28,10 +28,12 @@ import java.util.List; public class WiredEffectBotGiveHandItem extends InteractionWiredEffect { public static final WiredEffectType type = WiredEffectType.BOT_GIVE_HANDITEM; + private static final int BOT_SOURCE_NAME = 100; private String botName = ""; private int itemId; private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int botSource = BOT_SOURCE_NAME; public WiredEffectBotGiveHandItem(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -49,9 +51,10 @@ public class WiredEffectBotGiveHandItem extends InteractionWiredEffect { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(this.botName); - message.appendInt(2); + message.appendInt(3); message.appendInt(this.itemId); message.appendInt(this.userSource); + message.appendInt(this.botSource); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(this.getDelay()); @@ -80,11 +83,9 @@ public class WiredEffectBotGiveHandItem extends InteractionWiredEffect { public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { if(settings.getIntParams().length < 2) throw new WiredSaveException("Missing item id"); - int itemId = settings.getIntParams()[0]; - this.userSource = settings.getIntParams()[1]; - - if(itemId < 0) - itemId = 0; + int itemId = this.normalizeHandItem(settings.getIntParams()[0]); + this.userSource = this.normalizeUserSource(settings.getIntParams()[1]); + this.botSource = (settings.getIntParams().length > 2) ? this.normalizeBotSource(settings.getIntParams()[2]) : BOT_SOURCE_NAME; String botName = settings.getStringParam(); @@ -113,10 +114,9 @@ public class WiredEffectBotGiveHandItem extends InteractionWiredEffect { RoomUnit roomUnit = targets.get(0); Habbo habbo = room.getHabbo(roomUnit); - List bots = room.getBots(this.botName); + Bot bot = this.resolveBot(ctx, room); - if (habbo != null && bots.size() == 1) { - Bot bot = bots.get(0); + if (habbo != null && bot != null) { List tasks = new ArrayList<>(); tasks.add(new RoomUnitGiveHanditem(roomUnit, room, this.itemId)); @@ -146,7 +146,7 @@ public class WiredEffectBotGiveHandItem extends InteractionWiredEffect { @Override public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData(this.botName, this.itemId, this.getDelay(), this.userSource)); + return WiredManager.getGson().toJson(new JsonData(this.botName, this.itemId, this.getDelay(), this.userSource, this.botSource)); } @Override @@ -156,21 +156,25 @@ public class WiredEffectBotGiveHandItem extends InteractionWiredEffect { if(wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.setDelay(data.delay); - this.itemId = data.item_id; + this.itemId = this.normalizeHandItem(data.item_id); this.botName = data.bot_name; - this.userSource = data.userSource; + this.userSource = this.normalizeUserSource(data.userSource); + this.botSource = ((data.botSource == WiredSourceUtil.SOURCE_TRIGGER) && this.botName != null && !this.botName.isEmpty()) + ? BOT_SOURCE_NAME + : this.normalizeBotSource(data.botSource); } else { String[] data = wiredData.split(((char) 9) + ""); if (data.length == 3) { this.setDelay(Integer.parseInt(data[0])); - this.itemId = Integer.parseInt(data[1]); + this.itemId = this.normalizeHandItem(Integer.parseInt(data[1])); this.botName = data[2]; } this.needsUpdate(true); this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.botSource = BOT_SOURCE_NAME; } } @@ -179,12 +183,13 @@ public class WiredEffectBotGiveHandItem extends InteractionWiredEffect { this.botName = ""; this.itemId = 0; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.botSource = BOT_SOURCE_NAME; this.setDelay(0); } @Override public boolean requiresTriggeringUser() { - return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; + return (this.userSource == WiredSourceUtil.SOURCE_TRIGGER) || (this.botSource == WiredSourceUtil.SOURCE_TRIGGER); } static class JsonData { @@ -192,12 +197,51 @@ public class WiredEffectBotGiveHandItem extends InteractionWiredEffect { int item_id; int delay; int userSource; + int botSource; - public JsonData(String bot_name, int item_id, int delay, int userSource) { + public JsonData(String bot_name, int item_id, int delay, int userSource, int botSource) { this.bot_name = bot_name; this.item_id = item_id; this.delay = delay; this.userSource = userSource; + this.botSource = botSource; } } + + private int normalizeHandItem(int value) { + return Math.max(0, value); + } + + private int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private int normalizeBotSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_TRIGGER: + case BOT_SOURCE_NAME: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + return value; + default: + return BOT_SOURCE_NAME; + } + } + + private Bot resolveBot(WiredContext ctx, Room room) { + if (this.botSource == BOT_SOURCE_NAME) { + List bots = room.getBots(this.botName); + return (bots.size() == 1) ? bots.get(0) : null; + } + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.botSource)) { + Bot bot = room.getBot(roomUnit); + + if (bot != null) { + return bot; + } + } + + return null; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java index 8301f508..d027210b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java @@ -10,8 +10,10 @@ 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.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredBotSourceUtil; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredTextPlaceholderUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; @@ -26,6 +28,7 @@ public class WiredEffectBotTalk extends InteractionWiredEffect { private int mode; private String botName = ""; private String message = ""; + private int botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; public WiredEffectBotTalk(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -43,8 +46,9 @@ public class WiredEffectBotTalk extends InteractionWiredEffect { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(this.botName + "" + ((char) 9) + "" + this.message); - message.appendInt(1); + message.appendInt(2); message.appendInt(this.mode); + message.appendInt(this.botSource); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(this.getDelay()); @@ -55,6 +59,7 @@ public class WiredEffectBotTalk extends InteractionWiredEffect { public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { if(settings.getIntParams().length < 1) throw new WiredSaveException("Mode is invalid"); int mode = settings.getIntParams()[0]; + this.botSource = (settings.getIntParams().length > 1) ? WiredBotSourceUtil.normalizeBotSource(settings.getIntParams()[1]) : WiredBotSourceUtil.SOURCE_BOT_NAME; if(mode != 0 && mode != 1) throw new WiredSaveException("Mode is invalid"); @@ -103,21 +108,22 @@ public class WiredEffectBotTalk extends InteractionWiredEffect { .replace(Emulator.getTexts().getValue("wired.variable.points", "%points%"), habbo.getHabboInfo().getCurrencyAmount(Emulator.getConfig().getInt("seasonal.primary.type")) + "") .replace(Emulator.getTexts().getValue("wired.variable.owner", "%owner%"), room.getOwnerName()) .replace(Emulator.getTexts().getValue("wired.variable.item_count", "%item_count%"), room.itemCount() + "") - .replace(Emulator.getTexts().getValue("wired.variable.name", "%name%"), this.botName) .replace(Emulator.getTexts().getValue("wired.variable.roomname", "%roomname%"), room.getName()) .replace(Emulator.getTexts().getValue("wired.variable.user_count", "%user_count%"), room.getUserCount() + ""); } - List bots = room.getBots(this.botName); + message = WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, message); - if (bots.size() == 1) { - Bot bot = bots.get(0); + List bots = WiredBotSourceUtil.resolveBots(ctx, room, this.botSource, this.botName); - if(!WiredManager.triggerUserSays(room, bot.getRoomUnit(), message)) { + for (Bot bot : bots) { + String botMessage = message.replace(Emulator.getTexts().getValue("wired.variable.name", "%name%"), bot.getName()); + + if(!WiredManager.triggerUserSays(room, bot.getRoomUnit(), botMessage)) { if (this.mode == 1) { - bot.shout(message); + bot.shout(botMessage); } else { - bot.talk(message); + bot.talk(botMessage); } } } @@ -131,7 +137,7 @@ public class WiredEffectBotTalk extends InteractionWiredEffect { @Override public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData(this.botName, this.mode, this.message, this.getDelay())); + return WiredManager.getGson().toJson(new JsonData(this.botName, this.mode, this.message, this.getDelay(), this.botSource)); } @Override @@ -144,6 +150,9 @@ public class WiredEffectBotTalk extends InteractionWiredEffect { this.mode = data.mode; this.botName = data.bot_name; this.message = data.message; + this.botSource = (data.botSource != null) + ? WiredBotSourceUtil.normalizeBotSource(data.botSource) + : WiredBotSourceUtil.SOURCE_BOT_NAME; } else { String[] data = wiredData.split(((char) 9) + ""); @@ -156,6 +165,7 @@ public class WiredEffectBotTalk extends InteractionWiredEffect { } this.needsUpdate(true); + this.botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; } } @@ -164,9 +174,15 @@ public class WiredEffectBotTalk extends InteractionWiredEffect { this.mode = 0; this.botName = ""; this.message = ""; + this.botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; this.setDelay(0); } + @Override + public boolean requiresTriggeringUser() { + return WiredBotSourceUtil.requiresTriggeringUser(this.botSource) || WiredTextPlaceholderUtil.requiresActor(this.getRoom(), this); + } + public int getMode() { return this.mode; } @@ -201,12 +217,14 @@ public class WiredEffectBotTalk extends InteractionWiredEffect { int mode; String message; int delay; + Integer botSource; - public JsonData(String bot_name, int mode, String message, int delay) { + public JsonData(String bot_name, int mode, String message, int delay, int botSource) { this.bot_name = bot_name; this.mode = mode; this.message = message; this.delay = delay; + this.botSource = botSource; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java index c7dc2eb5..2673993c 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java @@ -11,9 +11,11 @@ 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.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredBotSourceUtil; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTextPlaceholderUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; import gnu.trove.procedure.TObjectProcedure; @@ -31,6 +33,7 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { private String botName = ""; private String message = ""; private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; public WiredEffectBotTalkToHabbo(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -48,9 +51,10 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(this.botName + "" + ((char) 9) + "" + this.message); - message.appendInt(2); + message.appendInt(3); message.appendInt(this.mode); message.appendInt(this.userSource); + message.appendInt(this.botSource); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(this.getDelay()); @@ -80,6 +84,7 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { if(settings.getIntParams().length < 2) throw new WiredSaveException("Missing mode"); int mode = settings.getIntParams()[0]; this.userSource = settings.getIntParams()[1]; + this.botSource = (settings.getIntParams().length > 2) ? WiredBotSourceUtil.normalizeBotSource(settings.getIntParams()[2]) : WiredBotSourceUtil.SOURCE_BOT_NAME; if(mode != 0 && mode != 1) throw new WiredSaveException("Mode is invalid"); @@ -116,9 +121,8 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { public void execute(WiredContext ctx) { Room room = ctx.room(); - List bots = room.getBots(this.botName); - if (bots.size() != 1) return; - Bot bot = bots.get(0); + List bots = WiredBotSourceUtil.resolveBots(ctx, room, this.botSource, this.botName); + if (bots.isEmpty()) return; for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.userSource)) { Habbo habbo = room.getHabbo(roomUnit); @@ -131,15 +135,19 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { .replace(Emulator.getTexts().getValue("wired.variable.points", "%points%"), habbo.getHabboInfo().getCurrencyAmount(Emulator.getConfig().getInt("seasonal.primary.type")) + "") .replace(Emulator.getTexts().getValue("wired.variable.owner", "%owner%"), room.getOwnerName()) .replace(Emulator.getTexts().getValue("wired.variable.item_count", "%item_count%"), room.itemCount() + "") - .replace(Emulator.getTexts().getValue("wired.variable.name", "%name%"), this.botName) .replace(Emulator.getTexts().getValue("wired.variable.roomname", "%roomname%"), room.getName()) .replace(Emulator.getTexts().getValue("wired.variable.user_count", "%user_count%"), room.getUserCount() + ""); + m = WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, m); - if (!WiredManager.triggerUserSays(room, bot.getRoomUnit(), m)) { - if (this.mode == 1) { - bot.whisper(m, habbo); - } else { - bot.talk(habbo.getHabboInfo().getUsername() + ": " + m); + for (Bot bot : bots) { + String botMessage = m.replace(Emulator.getTexts().getValue("wired.variable.name", "%name%"), bot.getName()); + + if (!WiredManager.triggerUserSays(room, bot.getRoomUnit(), botMessage)) { + if (this.mode == 1) { + bot.whisper(botMessage, habbo); + } else { + bot.talk(habbo.getHabboInfo().getUsername() + ": " + botMessage); + } } } } @@ -153,7 +161,7 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { @Override public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData(this.botName, this.mode, this.message, this.getDelay(), this.userSource)); + return WiredManager.getGson().toJson(new JsonData(this.botName, this.mode, this.message, this.getDelay(), this.userSource, this.botSource)); } @Override @@ -167,6 +175,7 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { this.botName = data.bot_name; this.message = data.message; this.userSource = data.userSource; + this.botSource = (data.botSource != null) ? WiredBotSourceUtil.normalizeBotSource(data.botSource) : WiredBotSourceUtil.SOURCE_BOT_NAME; } else { String[] data = wiredData.split(((char) 9) + ""); @@ -180,6 +189,7 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { this.needsUpdate(true); this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; } } @@ -189,12 +199,13 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { this.message = ""; this.mode = 0; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; this.setDelay(0); } @Override public boolean requiresTriggeringUser() { - return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; + return this.userSource == WiredSourceUtil.SOURCE_TRIGGER || WiredBotSourceUtil.requiresTriggeringUser(this.botSource) || WiredTextPlaceholderUtil.requiresActor(this.getRoom(), this); } static class JsonData { @@ -203,13 +214,15 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { String message; int delay; int userSource; + Integer botSource; - public JsonData(String bot_name, int mode, String message, int delay, int userSource) { + public JsonData(String bot_name, int mode, String message, int delay, int userSource, int botSource) { this.bot_name = bot_name; this.mode = mode; this.message = message; this.delay = delay; this.userSource = userSource; + this.botSource = botSource; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTeleport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTeleport.java index 650877d7..cb7d6d22 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTeleport.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTeleport.java @@ -12,6 +12,7 @@ import com.eu.habbo.habbohotel.rooms.RoomTileState; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredBotSourceUtil; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; @@ -34,6 +35,7 @@ public class WiredEffectBotTeleport extends InteractionWiredEffect { private THashSet items; private String botName = ""; private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private int botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; public WiredEffectBotTeleport(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -110,8 +112,9 @@ public class WiredEffectBotTeleport extends InteractionWiredEffect { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(this.botName); - message.appendInt(1); + message.appendInt(2); message.appendInt(this.furniSource); + message.appendInt(this.botSource); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(this.getDelay()); @@ -123,12 +126,17 @@ public class WiredEffectBotTeleport extends InteractionWiredEffect { String botName = settings.getStringParam(); int[] params = settings.getIntParams(); this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.botSource = (params.length > 1) ? WiredBotSourceUtil.normalizeBotSource(params[1]) : WiredBotSourceUtil.SOURCE_BOT_NAME; int itemsCount = settings.getFurniIds().length; if(itemsCount > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { throw new WiredSaveException("Too many furni selected"); } + if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + List newItems = new ArrayList<>(); if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { @@ -177,20 +185,20 @@ public class WiredEffectBotTeleport extends InteractionWiredEffect { if (validItems.isEmpty()) return; if (room.getLayout() == null) return; - List bots = room.getBots(this.botName); + List bots = WiredBotSourceUtil.resolveBots(ctx, room, this.botSource, this.botName); - if (bots.size() != 1) { + if (bots.isEmpty()) { return; } - Bot bot = bots.get(0); + for (Bot bot : bots) { + HabboItem targetItem = validItems.get(Emulator.getRandom().nextInt(validItems.size())); - HabboItem targetItem = validItems.get(Emulator.getRandom().nextInt(validItems.size())); - - if (targetItem.getRoomId() == bot.getRoom().getId()) { - RoomTile tile = room.getLayout().getTile(targetItem.getX(), targetItem.getY()); - if (tile != null) { - teleportUnitToTile(bot.getRoomUnit(), tile); + if (targetItem.getRoomId() == bot.getRoom().getId()) { + RoomTile tile = room.getLayout().getTile(targetItem.getX(), targetItem.getY()); + if (tile != null) { + teleportUnitToTile(bot.getRoomUnit(), tile); + } } } } @@ -214,7 +222,7 @@ public class WiredEffectBotTeleport extends InteractionWiredEffect { } } - return WiredManager.getGson().toJson(new JsonData(this.botName, itemIds, this.getDelay(), this.furniSource)); + return WiredManager.getGson().toJson(new JsonData(this.botName, itemIds, this.getDelay(), this.furniSource, this.botSource)); } @Override @@ -228,6 +236,9 @@ public class WiredEffectBotTeleport extends InteractionWiredEffect { this.setDelay(data.delay); this.botName = data.bot_name; this.furniSource = data.furniSource; + this.botSource = (data.botSource != null) + ? WiredBotSourceUtil.normalizeBotSource(data.botSource) + : WiredBotSourceUtil.SOURCE_BOT_NAME; for(int itemId : data.items) { HabboItem item = room.getHabboItem(itemId); @@ -260,6 +271,7 @@ public class WiredEffectBotTeleport extends InteractionWiredEffect { this.needsUpdate(true); this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; + this.botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; } } @@ -268,20 +280,28 @@ public class WiredEffectBotTeleport extends InteractionWiredEffect { this.botName = ""; this.items.clear(); this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; this.setDelay(0); } + @Override + public boolean requiresTriggeringUser() { + return WiredBotSourceUtil.requiresTriggeringUser(this.botSource); + } + static class JsonData { String bot_name; List items; int delay; int furniSource; + Integer botSource; - public JsonData(String bot_name, List items, int delay, int furniSource) { + public JsonData(String bot_name, List items, int delay, int furniSource, int botSource) { this.bot_name = bot_name; this.items = items; this.delay = delay; this.furniSource = furniSource; + this.botSource = botSource; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotWalkToFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotWalkToFurni.java index b8a2f059..0ccb96b6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotWalkToFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotWalkToFurni.java @@ -10,6 +10,7 @@ import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredBotSourceUtil; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; @@ -29,6 +30,7 @@ public class WiredEffectBotWalkToFurni extends InteractionWiredEffect { private List items; private String botName = ""; private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private int botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; public WiredEffectBotWalkToFurni(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -62,8 +64,9 @@ public class WiredEffectBotWalkToFurni extends InteractionWiredEffect { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(this.botName); - message.appendInt(1); + message.appendInt(2); message.appendInt(this.furniSource); + message.appendInt(this.botSource); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(this.getDelay()); @@ -75,12 +78,17 @@ public class WiredEffectBotWalkToFurni extends InteractionWiredEffect { String botName = settings.getStringParam(); int[] params = settings.getIntParams(); this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.botSource = (params.length > 1) ? WiredBotSourceUtil.normalizeBotSource(params[1]) : WiredBotSourceUtil.SOURCE_BOT_NAME; int itemsCount = settings.getFurniIds().length; if(itemsCount > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { throw new WiredSaveException("Too many furni selected"); } + if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + List newItems = new ArrayList<>(); if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { @@ -118,32 +126,30 @@ public class WiredEffectBotWalkToFurni extends InteractionWiredEffect { @Override public void execute(WiredContext ctx) { Room room = ctx.room(); - List bots = room.getBots(this.botName); + List bots = WiredBotSourceUtil.resolveBots(ctx, room, this.botSource, this.botName); List effectiveItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { this.items.removeIf(item -> item == null || item.getRoomId() != this.getRoomId() || Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()).getHabboItem(item.getId()) == null); } - if (effectiveItems.isEmpty() || bots.size() != 1) { + if (effectiveItems.isEmpty() || bots.isEmpty()) { return; } - Bot bot = bots.get(0); + for (Bot bot : bots) { + List possibleItems = effectiveItems.stream() + .filter(item -> !room.getBotsOnItem(item).contains(bot)) + .collect(Collectors.toList()); - // Bots shouldn't walk to the tile they are already standing on - List possibleItems = effectiveItems.stream() - .filter(item -> !room.getBotsOnItem(item).contains(bot)) - .collect(Collectors.toList()); + if (possibleItems.isEmpty()) { + continue; + } - // Get a random tile of possible tiles to walk to - if (possibleItems.size() > 0) { HabboItem item = possibleItems.get(Emulator.getRandom().nextInt(possibleItems.size())); - if (item.getRoomId() != 0 && item.getRoomId() == bot.getRoom().getId()) { - if (room.getLayout() != null) { - bot.getRoomUnit().setGoalLocation(room.getLayout().getTile(item.getX(), item.getY())); - } + if (item.getRoomId() != 0 && item.getRoomId() == bot.getRoom().getId() && room.getLayout() != null) { + bot.getRoomUnit().setGoalLocation(room.getLayout().getTile(item.getX(), item.getY())); } } } @@ -166,7 +172,7 @@ public class WiredEffectBotWalkToFurni extends InteractionWiredEffect { } } - return WiredManager.getGson().toJson(new JsonData(this.botName, itemIds, this.getDelay(), this.furniSource)); + return WiredManager.getGson().toJson(new JsonData(this.botName, itemIds, this.getDelay(), this.furniSource, this.botSource)); } @Override @@ -180,6 +186,9 @@ public class WiredEffectBotWalkToFurni extends InteractionWiredEffect { this.setDelay(data.delay); this.botName = data.bot_name; this.furniSource = data.furniSource; + this.botSource = (data.botSource != null) + ? WiredBotSourceUtil.normalizeBotSource(data.botSource) + : WiredBotSourceUtil.SOURCE_BOT_NAME; for(int itemId : data.items) { HabboItem item = room.getHabboItem(itemId); @@ -212,6 +221,7 @@ public class WiredEffectBotWalkToFurni extends InteractionWiredEffect { this.needsUpdate(true); this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; + this.botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; } } @@ -220,20 +230,28 @@ public class WiredEffectBotWalkToFurni extends InteractionWiredEffect { this.items.clear(); this.botName = ""; this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.botSource = WiredBotSourceUtil.SOURCE_BOT_NAME; this.setDelay(0); } + @Override + public boolean requiresTriggeringUser() { + return WiredBotSourceUtil.requiresTriggeringUser(this.botSource); + } + static class JsonData { String bot_name; List items; int delay; int furniSource; + Integer botSource; - public JsonData(String bot_name, List items, int delay, int furniSource) { + public JsonData(String bot_name, List items, int delay, int furniSource, int botSource) { this.bot_name = bot_name; this.items = items; this.delay = delay; this.furniSource = furniSource; + this.botSource = botSource; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeFurniDirection.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeFurniDirection.java index 529f206b..58756370 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeFurniDirection.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeFurniDirection.java @@ -10,10 +10,10 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.*; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; -import com.eu.habbo.messages.outgoing.rooms.items.FloorItemOnRollerComposer; import gnu.trove.map.hash.THashMap; import gnu.trove.set.hash.THashSet; @@ -22,6 +22,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { public static final int ACTION_WAIT = 0; @@ -35,9 +36,11 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { public static final WiredEffectType type = WiredEffectType.MOVE_DIRECTION; private final THashMap items = new THashMap<>(0); + private final ConcurrentHashMap runtimeItems = new ConcurrentHashMap<>(); private RoomUserRotation startRotation = RoomUserRotation.NORTH; private int blockedAction = 0; private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private boolean blockOnUserCollision = false; public WiredEffectChangeFurniDirection(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -56,6 +59,7 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { THashMap effectiveItems; if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + this.runtimeItems.clear(); THashSet toRemove = new THashSet<>(); for (HabboItem item : this.items.keySet()) { if (item == null || Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()).getHabboItem(item.getId()) == null) @@ -66,10 +70,13 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { } effectiveItems = this.items; } else { + this.pruneRuntimeItems(room); effectiveItems = new THashMap<>(); for (HabboItem item : resolvedItems) { if (item != null) { - WiredChangeDirectionSetting setting = this.items.get(item); + WiredChangeDirectionSetting setting = this.runtimeItems.computeIfAbsent( + item.getId(), + key -> new WiredChangeDirectionSetting(item.getId(), item.getRotation(), this.startRotation)); if (setting == null) { setting = new WiredChangeDirectionSetting(item.getId(), item.getRotation(), this.startRotation); } @@ -90,7 +97,7 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { RoomTile targetTile = room.getLayout().getTileInFront(itemTile, entry.getValue().direction.getValue()); int count = 1; - while ((targetTile == null || targetTile.state == RoomTileState.INVALID || room.furnitureFitsAt(targetTile, item, item.getRotation(), false) != FurnitureMovementError.NONE) && count < 8) { + while (this.shouldSearchNextDirection(room, item, targetTile, ctx) && count < 8) { entry.getValue().direction = this.nextRotation(entry.getValue().direction); RoomTile tile = room.getLayout().getTileInFront(itemTile, entry.getValue().direction.getValue()); @@ -114,35 +121,30 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { RoomTile targetTile = room.getLayout().getTileInFront(itemTile, newDirection); if(item.getRotation() != entry.getValue().rotation) { - if(targetTile == null || room.furnitureFitsAt(targetTile, item, entry.getValue().rotation, false) != FurnitureMovementError.NONE) + if (targetTile == null || room.furnitureFitsAt(targetTile, item, entry.getValue().rotation, false) != FurnitureMovementError.NONE) continue; - room.moveFurniTo(entry.getKey(), targetTile, entry.getValue().rotation, null, true); + WiredMoveCarryHelper.moveFurni(room, this, entry.getKey(), targetTile, entry.getValue().rotation, null, true, ctx); } if (targetTile == null) continue; - boolean hasRoomUnits = false; - THashSet newOccupiedTiles = room.getLayout().getTilesAt(targetTile, item.getBaseItem().getWidth(), item.getBaseItem().getLength(), item.getRotation()); - for(RoomTile tile : newOccupiedTiles) { - for (RoomUnit _roomUnit : room.getRoomUnits(tile)) { - hasRoomUnits = true; - if(_roomUnit.getCurrentLocation() == targetTile) { - Emulator.getThreading().run(() -> { - WiredManager.triggerBotCollision(room, _roomUnit); - }); + FurnitureMovementError movementError = WiredMoveCarryHelper.getMovementError(room, this, entry.getKey(), targetTile, item.getRotation(), ctx); + + if (movementError == FurnitureMovementError.TILE_HAS_HABBOS + || movementError == FurnitureMovementError.TILE_HAS_BOTS + || movementError == FurnitureMovementError.TILE_HAS_PETS) { + Emulator.getThreading().run(() -> { + for (RoomUnit roomUnit : room.getRoomUnits(targetTile)) { + WiredManager.triggerBotCollision(room, roomUnit); break; } - } + }); } - if (targetTile.state != RoomTileState.INVALID && room.furnitureFitsAt(targetTile, item, item.getRotation(), false) == FurnitureMovementError.NONE) { - if (!hasRoomUnits) { - RoomTile oldLocation = room.getLayout().getTile(entry.getKey().getX(), entry.getKey().getY()); - double oldZ = entry.getKey().getZ(); - if(oldLocation != null && room.moveFurniTo(entry.getKey(), targetTile, item.getRotation(), null, false) == FurnitureMovementError.NONE) { - room.sendComposer(new FloorItemOnRollerComposer(entry.getKey(), null, oldLocation, oldZ, targetTile, entry.getKey().getZ(), 0, room).compose()); - } + if (targetTile.state != RoomTileState.INVALID && movementError == FurnitureMovementError.NONE) { + RoomTile oldLocation = room.getLayout().getTile(entry.getKey().getX(), entry.getKey().getY()); + if (oldLocation != null && WiredMoveCarryHelper.moveFurni(room, this, entry.getKey(), targetTile, item.getRotation(), null, false, ctx) == FurnitureMovementError.NONE) { } } } @@ -157,13 +159,14 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { @Override public String getWiredData() { ArrayList settings = new ArrayList<>(this.items.values()); - return WiredManager.getGson().toJson(new JsonData(this.startRotation, this.blockedAction, settings, this.getDelay(), this.furniSource)); + return WiredManager.getGson().toJson(new JsonData(this.startRotation, this.blockedAction, settings, this.getDelay(), this.furniSource, this.blockOnUserCollision)); } @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { this.items.clear(); + this.runtimeItems.clear(); String wiredData = set.getString("wired_data"); @@ -173,6 +176,7 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { this.startRotation = data.start_direction; this.blockedAction = data.blocked_action; this.furniSource = data.furniSource; + this.blockOnUserCollision = data.blockOnUserCollision; for(WiredChangeDirectionSetting setting : data.items) { HabboItem item = room.getHabboItem(setting.item_id); @@ -217,6 +221,7 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { } this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; + this.blockOnUserCollision = false; this.needsUpdate(true); } } @@ -225,9 +230,11 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { public void onPickUp() { this.setDelay(0); this.items.clear(); + this.runtimeItems.clear(); this.blockedAction = 0; this.startRotation = RoomUserRotation.NORTH; this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.blockOnUserCollision = false; } @Override @@ -246,10 +253,11 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(3); + message.appendInt(4); message.appendInt(this.startRotation != null ? this.startRotation.getValue() : 0); message.appendInt(this.blockedAction); message.appendInt(this.furniSource); + message.appendInt(this.blockOnUserCollision ? 1 : 0); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(this.getDelay()); @@ -262,7 +270,7 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { int startDirectionInt = settings.getIntParams()[0]; - if(startDirectionInt < 0 || startDirectionInt > 7 || (startDirectionInt % 2) != 0) { + if(startDirectionInt < 0 || startDirectionInt > 7) { throw new WiredSaveException("Start direction is invalid"); } @@ -270,6 +278,7 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { int blockedActionInt = settings.getIntParams()[1]; this.furniSource = settings.getIntParams()[2]; + this.blockOnUserCollision = settings.getIntParams().length > 3 && settings.getIntParams()[3] == 1; if(blockedActionInt < 0 || blockedActionInt > 6) { throw new WiredSaveException("Blocked action is invalid"); @@ -281,6 +290,10 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { throw new WiredSaveException("Too many furni selected"); } + if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + THashMap newItems = new THashMap<>(); for (int i = 0; i < itemsCount; i++) { @@ -299,6 +312,7 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { throw new WiredSaveException("Delay too long"); this.items.clear(); + this.runtimeItems.clear(); if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { this.items.putAll(newItems); } @@ -309,6 +323,31 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { return true; } + private void pruneRuntimeItems(Room room) { + if (room == null || this.runtimeItems.isEmpty()) { + return; + } + + this.runtimeItems.entrySet().removeIf(entry -> room.getHabboItem(entry.getKey()) == null); + } + + private boolean shouldSearchNextDirection(Room room, HabboItem item, RoomTile targetTile, WiredContext ctx) { + if (targetTile == null || targetTile.state == RoomTileState.INVALID) { + return true; + } + + if (room.furnitureFitsAt(targetTile, item, item.getRotation(), false) != FurnitureMovementError.NONE) { + return true; + } + + if (this.blockOnUserCollision) { + return false; + } + + FurnitureMovementError unitCollision = WiredMoveCarryHelper.getMovementError(room, this, item, targetTile, item.getRotation(), ctx); + return unitCollision == FurnitureMovementError.TILE_HAS_HABBOS; + } + private RoomUserRotation nextRotation(RoomUserRotation currentRotation) { switch (this.blockedAction) { case ACTION_TURN_BACK: @@ -340,13 +379,15 @@ public class WiredEffectChangeFurniDirection extends InteractionWiredEffect { List items; int delay; int furniSource; + boolean blockOnUserCollision; - public JsonData(RoomUserRotation start_direction, int blocked_action, List items, int delay, int furniSource) { + public JsonData(RoomUserRotation start_direction, int blocked_action, List items, int delay, int furniSource, boolean blockOnUserCollision) { this.start_direction = start_direction; this.blocked_action = blocked_action; this.items = items; this.delay = delay; this.furniSource = furniSource; + this.blockOnUserCollision = blockOnUserCollision; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java new file mode 100644 index 00000000..5e7e5c37 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java @@ -0,0 +1,946 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame; +import com.eu.habbo.habbohotel.games.freeze.FreezeGame; +import com.eu.habbo.habbohotel.games.wired.WiredGame; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.FurnitureMovementError; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomTileState; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUserRotation; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredInternalVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer; +import com.eu.habbo.util.HotelDateTimeUtil; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; + +public class WiredEffectChangeVariableValue extends InteractionWiredEffect { + public static final WiredEffectType type = WiredEffectType.CHANGE_VAR_VAL; + public static final int TARGET_USER = 0, TARGET_FURNI = 1, TARGET_CONTEXT = 2, TARGET_ROOM = 3; + public static final int REF_CONSTANT = 0, REF_VARIABLE = 1; + public static final int OP_ASSIGN = 0, OP_ADD = 1, OP_SUB = 2, OP_MUL = 3, OP_DIV = 4, OP_POW = 5, OP_MOD = 6, OP_MIN = 40, OP_MAX = 41, OP_RANDOM = 50, OP_ABS = 60, OP_AND = 100, OP_OR = 101, OP_XOR = 102, OP_NOT = 103, OP_LSHIFT = 104, OP_RSHIFT = 105; + + private static final int SOURCE_SECONDARY_SELECTED = 101; + private static final String DELIM = "\t", FURNI_DELIM = ";"; + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + private static final String INTERNAL_TOKEN_PREFIX = "internal:"; + + private int destinationTargetType = TARGET_USER, destinationVariableItemId = 0, operation = OP_ASSIGN, referenceMode = REF_CONSTANT, referenceConstantValue = 0, referenceTargetType = TARGET_USER, referenceVariableItemId = 0, destinationUserSource = WiredSourceUtil.SOURCE_TRIGGER, destinationFurniSource = WiredSourceUtil.SOURCE_TRIGGER, referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER, referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + private String destinationVariableToken = "", referenceVariableToken = ""; + private final List destinationSelectedFurni = new ArrayList<>(); + private final List referenceSelectedFurni = new ArrayList<>(); + + public WiredEffectChangeVariableValue(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectChangeVariableValue(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) return; + + switch (this.destinationTargetType) { + case TARGET_USER -> this.executeUsers(ctx, room); + case TARGET_FURNI -> this.executeFurni(ctx, room); + case TARGET_CONTEXT -> this.executeContext(ctx, room); + case TARGET_ROOM -> this.executeRoom(ctx, room); + } + } + + private void executeUsers(WiredContext ctx, Room room) { + if (isInternalVariableToken(this.destinationVariableToken)) { + this.executeUsersInternal(ctx, room, getInternalVariableKey(this.destinationVariableToken)); + return; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.destinationVariableItemId); + if (definition == null || !definition.hasValue() || definition.isReadOnly()) return; + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + int index = 0; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.destinationUserSource)) { + if (roomUnit == null) continue; + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo == null) continue; + + Integer referenceValue = this.referenceFor(references, roomUnit.getId(), TARGET_USER, index++); + if (!this.isUnaryOperation() && referenceValue == null) continue; + + int currentValue = room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.destinationVariableItemId); + room.getUserVariableManager().updateVariableValue(habbo.getHabboInfo().getId(), this.destinationVariableItemId, this.applyOperation(currentValue, referenceValue)); + } + } + + private void executeUsersInternal(WiredContext ctx, Room room, String key) { + if (!canUseUserInternalDestination(key)) return; + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + int index = 0; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.destinationUserSource)) { + if (roomUnit == null) continue; + + Integer currentValue = this.readUserInternalValue(room, roomUnit, key); + if (currentValue == null) continue; + + Integer referenceValue = this.referenceFor(references, roomUnit.getId(), TARGET_USER, index++); + if (!this.isUnaryOperation() && referenceValue == null) continue; + + this.writeUserInternalValue(room, roomUnit, key, this.applyOperation(currentValue, referenceValue)); + } + } + + private void executeFurni(WiredContext ctx, Room room) { + if (isInternalVariableToken(this.destinationVariableToken)) { + this.executeFurniInternal(ctx, room, getInternalVariableKey(this.destinationVariableToken)); + return; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.destinationVariableItemId); + if (definition == null || !definition.hasValue() || definition.isReadOnly()) return; + if (this.destinationFurniSource == WiredSourceUtil.SOURCE_SELECTED) this.validateItems(this.destinationSelectedFurni); + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + int index = 0; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, this.destinationFurniSource, this.destinationSelectedFurni)) { + if (item == null) continue; + + Integer referenceValue = this.referenceFor(references, item.getId(), TARGET_FURNI, index++); + if (!this.isUnaryOperation() && referenceValue == null) continue; + + int currentValue = room.getFurniVariableManager().getCurrentValue(item.getId(), this.destinationVariableItemId); + room.getFurniVariableManager().updateVariableValue(item.getId(), this.destinationVariableItemId, this.applyOperation(currentValue, referenceValue)); + } + } + + private void executeFurniInternal(WiredContext ctx, Room room, String key) { + if (!canUseFurniInternalDestination(key)) return; + if (this.destinationFurniSource == WiredSourceUtil.SOURCE_SELECTED) this.validateItems(this.destinationSelectedFurni); + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + int index = 0; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, this.destinationFurniSource, this.destinationSelectedFurni)) { + if (item == null) continue; + + Integer currentValue = this.readFurniInternalValue(room, item, key); + if (currentValue == null) continue; + + Integer referenceValue = this.referenceFor(references, item.getId(), TARGET_FURNI, index++); + if (!this.isUnaryOperation() && referenceValue == null) continue; + + this.writeFurniInternalValue(room, item, key, this.applyOperation(currentValue, referenceValue)); + } + } + + private void executeRoom(WiredContext ctx, Room room) { + if (isInternalVariableToken(this.destinationVariableToken)) return; + + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(this.destinationVariableItemId); + if (definition == null || !definition.hasValue() || definition.isReadOnly()) return; + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + Integer referenceValue = this.referenceFor(references, room.getId(), TARGET_ROOM, 0); + if (!this.isUnaryOperation() && referenceValue == null) return; + + int currentValue = room.getRoomVariableManager().getCurrentValue(this.destinationVariableItemId); + room.getRoomVariableManager().updateVariableValue(this.destinationVariableItemId, this.applyOperation(currentValue, referenceValue)); + } + + private void executeContext(WiredContext ctx, Room room) { + if (isInternalVariableToken(this.destinationVariableToken)) return; + + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, this.destinationVariableItemId); + if (definition == null || !definition.hasValue() || definition.isReadOnly()) return; + + ReferenceSnapshot references = this.resolveReferences(ctx, room); + Integer referenceValue = this.referenceFor(references, this.destinationVariableItemId, TARGET_CONTEXT, 0); + if (!this.isUnaryOperation() && referenceValue == null) return; + if (!WiredContextVariableSupport.hasVariable(ctx, this.destinationVariableItemId)) return; + + Integer currentValue = WiredContextVariableSupport.getCurrentValue(ctx, this.destinationVariableItemId); + int nextValue = this.applyOperation(currentValue != null ? currentValue : 0, referenceValue); + WiredContextVariableSupport.updateVariableValue(ctx, room, this.destinationVariableItemId, nextValue); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.validateItems(this.destinationSelectedFurni); + this.validateItems(this.referenceSelectedFurni); + + List selectedItems = new ArrayList<>(); + if (this.destinationTargetType == TARGET_FURNI && this.destinationFurniSource == WiredSourceUtil.SOURCE_SELECTED) selectedItems.addAll(this.destinationSelectedFurni); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(selectedItems.size()); + for (HabboItem item : selectedItems) message.appendInt(item.getId()); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.serializeStringData()); + message.appendInt(9); + message.appendInt(this.destinationTargetType); + message.appendInt(this.operation); + message.appendInt(this.referenceMode); + message.appendInt(this.referenceConstantValue); + message.appendInt(this.referenceTargetType); + message.appendInt(this.destinationUserSource); + message.appendInt(this.destinationFurniSource); + message.appendInt(this.referenceUserSource); + message.appendInt(this.referenceFurniSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = this.getRoom(); + if (room == null) throw new WiredSaveException("Room not found"); + + int[] params = settings.getIntParams(); + String[] stringParts = this.parseStringData(settings.getStringParam()); + int nextDestinationTargetType = normalizeTargetType(param(params, 0, TARGET_USER)); + int nextOperation = normalizeOperation(param(params, 1, OP_ASSIGN)); + int nextReferenceMode = normalizeReferenceMode(param(params, 2, REF_CONSTANT)); + int nextReferenceConstantValue = param(params, 3, 0); + int nextReferenceTargetType = normalizeTargetType(param(params, 4, TARGET_USER)); + int nextDestinationUserSource = normalizeUserSource(param(params, 5, WiredSourceUtil.SOURCE_TRIGGER)); + int nextDestinationFurniSource = normalizeDestinationFurniSource(param(params, 6, WiredSourceUtil.SOURCE_TRIGGER)); + int nextReferenceUserSource = normalizeUserSource(param(params, 7, WiredSourceUtil.SOURCE_TRIGGER)); + int nextReferenceFurniSource = normalizeReferenceFurniSource(param(params, 8, WiredSourceUtil.SOURCE_TRIGGER)); + String nextDestinationVariableToken = normalizeVariableToken((stringParts.length > 0) ? stringParts[0] : ""); + String nextReferenceVariableToken = normalizeVariableToken((stringParts.length > 1) ? stringParts[1] : ""); + + this.validateDestination(room, nextDestinationTargetType, nextDestinationVariableToken); + if (nextReferenceMode == REF_VARIABLE) this.validateReference(room, nextReferenceTargetType, nextReferenceVariableToken); + + int maxDelay = Emulator.getConfig().getInt("hotel.wired.max_delay", 20); + if (settings.getDelay() > maxDelay) throw new WiredSaveException("Delay too long"); + + List nextDestinationItems = (nextDestinationTargetType == TARGET_FURNI && nextDestinationFurniSource == WiredSourceUtil.SOURCE_SELECTED) ? this.parseItems(settings.getFurniIds(), room) : new ArrayList<>(); + List nextReferenceItems = (nextReferenceMode == REF_VARIABLE && nextReferenceTargetType == TARGET_FURNI && nextReferenceFurniSource == SOURCE_SECONDARY_SELECTED) ? this.parseItems((stringParts.length > 2) ? stringParts[2] : "", room) : new ArrayList<>(); + int selectionLimit = Emulator.getConfig().getInt("hotel.wired.furni.selection.count"); + if (nextDestinationItems.size() > selectionLimit || nextReferenceItems.size() > selectionLimit) throw new WiredSaveException("Too many furni selected"); + + this.destinationSelectedFurni.clear(); + this.destinationSelectedFurni.addAll(nextDestinationItems); + this.referenceSelectedFurni.clear(); + this.referenceSelectedFurni.addAll(nextReferenceItems); + this.destinationTargetType = nextDestinationTargetType; + this.setDestinationVariableToken(nextDestinationVariableToken); + this.operation = nextOperation; + this.referenceMode = nextReferenceMode; + this.referenceConstantValue = nextReferenceConstantValue; + this.referenceTargetType = nextReferenceTargetType; + this.setReferenceVariableToken(nextReferenceVariableToken); + this.destinationUserSource = nextDestinationUserSource; + this.destinationFurniSource = nextDestinationFurniSource; + this.referenceUserSource = nextReferenceUserSource; + this.referenceFurniSource = nextReferenceFurniSource; + this.setDelay(settings.getDelay()); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.destinationTargetType, this.destinationVariableToken, this.destinationVariableItemId, this.operation, this.referenceMode, this.referenceConstantValue, this.referenceTargetType, this.referenceVariableToken, this.referenceVariableItemId, this.destinationUserSource, this.destinationFurniSource, this.referenceUserSource, this.referenceFurniSource, this.getDelay(), this.toIds(this.destinationSelectedFurni), this.toIds(this.referenceSelectedFurni))); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) return; + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) return; + + this.destinationTargetType = normalizeTargetType(data.destinationTargetType); + this.setDestinationVariableToken(normalizeVariableToken((data.destinationVariableToken != null) ? data.destinationVariableToken : ((data.destinationVariableItemId > 0) ? String.valueOf(data.destinationVariableItemId) : ""))); + this.operation = normalizeOperation(data.operation); + this.referenceMode = normalizeReferenceMode(data.referenceMode); + this.referenceConstantValue = data.referenceConstantValue; + this.referenceTargetType = normalizeTargetType(data.referenceTargetType); + this.setReferenceVariableToken(normalizeVariableToken((data.referenceVariableToken != null) ? data.referenceVariableToken : ((data.referenceVariableItemId > 0) ? String.valueOf(data.referenceVariableItemId) : ""))); + this.destinationUserSource = normalizeUserSource(data.destinationUserSource); + this.destinationFurniSource = normalizeDestinationFurniSource(data.destinationFurniSource); + this.referenceUserSource = normalizeUserSource(data.referenceUserSource); + this.referenceFurniSource = normalizeReferenceFurniSource(data.referenceFurniSource); + this.setDelay(Math.max(0, data.delay)); + + if (room != null) { + try { + this.destinationSelectedFurni.addAll(this.parseItems(data.destinationSelectedFurniIds, room)); + this.referenceSelectedFurni.addAll(this.parseItems(data.referenceSelectedFurniIds, room)); + } catch (WiredSaveException ignored) { + } + } + } + + @Override + public void onPickUp() { + this.destinationTargetType = TARGET_USER; + this.setDestinationVariableToken(""); + this.operation = OP_ASSIGN; + this.referenceMode = REF_CONSTANT; + this.referenceConstantValue = 0; + this.referenceTargetType = TARGET_USER; + this.setReferenceVariableToken(""); + this.destinationUserSource = WiredSourceUtil.SOURCE_TRIGGER; + this.destinationFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER; + this.referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.destinationSelectedFurni.clear(); + this.referenceSelectedFurni.clear(); + this.setDelay(0); + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public boolean requiresTriggeringUser() { + return (this.destinationTargetType == TARGET_USER && this.destinationUserSource == WiredSourceUtil.SOURCE_TRIGGER) + || (this.referenceMode == REF_VARIABLE && this.referenceTargetType == TARGET_USER && this.referenceUserSource == WiredSourceUtil.SOURCE_TRIGGER); + } + + private ReferenceSnapshot resolveReferences(WiredContext ctx, Room room) { + if (this.referenceMode != REF_VARIABLE) return null; + + return switch (this.referenceTargetType) { + case TARGET_USER -> this.userReferences(ctx, room); + case TARGET_FURNI -> this.furniReferences(ctx, room); + case TARGET_CONTEXT -> this.contextReferences(ctx, room); + case TARGET_ROOM -> this.roomReferences(room); + default -> null; + }; + } + + private ReferenceSnapshot userReferences(WiredContext ctx, Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_USER); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseUserInternalReference(key)) return null; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.referenceUserSource)) { + if (roomUnit == null) continue; + + Integer value = this.readUserInternalValue(room, roomUnit, key); + if (value != null) snapshot.add(roomUnit.getId(), value); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.referenceUserSource)) { + if (roomUnit == null) continue; + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo != null) snapshot.add(roomUnit.getId(), room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.referenceVariableItemId)); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + private ReferenceSnapshot furniReferences(WiredContext ctx, Room room) { + int source = (this.referenceFurniSource == SOURCE_SECONDARY_SELECTED) ? WiredSourceUtil.SOURCE_SELECTED : this.referenceFurniSource; + if (source == WiredSourceUtil.SOURCE_SELECTED) this.validateItems(this.referenceSelectedFurni); + + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_FURNI); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseFurniInternalReference(key)) return null; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, source, this.referenceSelectedFurni)) { + if (item == null) continue; + + Integer value = this.readFurniInternalValue(room, item, key); + if (value != null) snapshot.add(item.getId(), value); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, source, this.referenceSelectedFurni)) { + if (item != null) snapshot.add(item.getId(), room.getFurniVariableManager().getCurrentValue(item.getId(), this.referenceVariableItemId)); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + private ReferenceSnapshot roomReferences(Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_ROOM); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseRoomInternalReference(key)) return null; + + Integer value = this.readRoomInternalValue(room, key); + if (value == null) return null; + + snapshot.add(room.getId(), value); + return snapshot; + } + + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + snapshot.add(room.getId(), room.getRoomVariableManager().getCurrentValue(this.referenceVariableItemId)); + return snapshot; + } + + private ReferenceSnapshot contextReferences(WiredContext ctx, Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_CONTEXT); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseContextInternalReference(key)) return null; + + Integer value = WiredInternalVariableSupport.readContextValue(ctx, key); + if (value == null) return null; + + snapshot.add(this.referenceVariableItemId > 0 ? this.referenceVariableItemId : room.getId(), value); + return snapshot; + } + + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, this.referenceVariableItemId); + if (definition == null || !definition.hasValue() || !WiredContextVariableSupport.hasVariable(ctx, this.referenceVariableItemId)) return null; + + Integer value = WiredContextVariableSupport.getCurrentValue(ctx, this.referenceVariableItemId); + if (value == null) return null; + + snapshot.add(this.referenceVariableItemId, value); + return snapshot; + } + + private Integer referenceFor(ReferenceSnapshot snapshot, int destinationEntityId, int destinationTarget, int destinationIndex) { + if (this.referenceMode != REF_VARIABLE) return this.referenceConstantValue; + if (this.isUnaryOperation()) return 0; + if (snapshot == null || snapshot.isEmpty()) return null; + if (snapshot.targetType == destinationTarget && snapshot.values.containsKey(destinationEntityId)) return snapshot.values.get(destinationEntityId); + if (destinationIndex >= 0 && destinationIndex < snapshot.values.size()) return new ArrayList<>(snapshot.values.values()).get(destinationIndex); + return new ArrayList<>(snapshot.values.values()).get(0); + } + + private int applyOperation(int currentValue, Integer referenceValue) { + return switch (this.operation) { + case OP_ASSIGN -> (referenceValue != null) ? referenceValue : currentValue; + case OP_ADD -> clamp((long) currentValue + referenceValue); + case OP_SUB -> clamp((long) currentValue - referenceValue); + case OP_MUL -> clamp((long) currentValue * referenceValue); + case OP_DIV -> (referenceValue == null || referenceValue == 0) ? currentValue : (currentValue / referenceValue); + case OP_POW -> (referenceValue == null || referenceValue < 0) ? 0 : clamp(Math.round(Math.pow(currentValue, referenceValue))); + case OP_MOD -> (referenceValue == null || referenceValue == 0) ? currentValue : (currentValue % referenceValue); + case OP_MIN -> (referenceValue != null) ? Math.min(currentValue, referenceValue) : currentValue; + case OP_MAX -> (referenceValue != null) ? Math.max(currentValue, referenceValue) : currentValue; + case OP_RANDOM -> (referenceValue == null || referenceValue <= 0) ? 0 : Emulator.getRandom().nextInt(referenceValue + 1); + case OP_ABS -> (currentValue == Integer.MIN_VALUE) ? Integer.MAX_VALUE : Math.abs(currentValue); + case OP_AND -> (referenceValue != null) ? (currentValue & referenceValue) : currentValue; + case OP_OR -> (referenceValue != null) ? (currentValue | referenceValue) : currentValue; + case OP_XOR -> (referenceValue != null) ? (currentValue ^ referenceValue) : currentValue; + case OP_NOT -> ~currentValue; + case OP_LSHIFT -> currentValue << shift(referenceValue); + case OP_RSHIFT -> currentValue >> shift(referenceValue); + default -> currentValue; + }; + } + + private boolean isUnaryOperation() { + return this.operation == OP_ABS || this.operation == OP_NOT; + } + + private void validateDestination(Room room, int targetType, String variableToken) throws WiredSaveException { + if (variableToken == null || variableToken.isEmpty()) throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + + boolean valid = switch (targetType) { + case TARGET_USER -> isInternalVariableToken(variableToken) + ? canUseUserInternalDestination(getInternalVariableKey(variableToken)) + : this.isValidUserCustomDestination(room, getCustomItemId(variableToken)); + case TARGET_FURNI -> isInternalVariableToken(variableToken) + ? canUseFurniInternalDestination(getInternalVariableKey(variableToken)) + : this.isValidFurniCustomDestination(room, getCustomItemId(variableToken)); + case TARGET_CONTEXT -> !isInternalVariableToken(variableToken) + && this.isValidContextCustomDestination(room, getCustomItemId(variableToken)); + case TARGET_ROOM -> !isInternalVariableToken(variableToken) && this.isValidRoomCustomDestination(room, getCustomItemId(variableToken)); + default -> false; + }; + + if (!valid) throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + private void validateReference(Room room, int targetType, String variableToken) throws WiredSaveException { + if (variableToken == null || variableToken.isEmpty()) throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + + boolean valid = switch (targetType) { + case TARGET_USER -> isInternalVariableToken(variableToken) + ? canUseUserInternalReference(getInternalVariableKey(variableToken)) + : this.isValidUserCustomReference(room, getCustomItemId(variableToken)); + case TARGET_FURNI -> isInternalVariableToken(variableToken) + ? canUseFurniInternalReference(getInternalVariableKey(variableToken)) + : this.isValidFurniCustomDestination(room, getCustomItemId(variableToken)); + case TARGET_CONTEXT -> isInternalVariableToken(variableToken) + ? canUseContextInternalReference(getInternalVariableKey(variableToken)) + : this.isValidContextCustomReference(room, getCustomItemId(variableToken)); + case TARGET_ROOM -> isInternalVariableToken(variableToken) + ? canUseRoomInternalReference(getInternalVariableKey(variableToken)) + : this.isValidRoomCustomReference(room, getCustomItemId(variableToken)); + default -> false; + }; + + if (!valid) throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + private boolean isValidUserCustomDestination(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getUserVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue() && !definition.isReadOnly(); + } + + private boolean isValidFurniCustomDestination(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getFurniVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue() && !definition.isReadOnly(); + } + + private boolean isValidRoomCustomDestination(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getRoomVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue() && !definition.isReadOnly(); + } + + private boolean isValidContextCustomDestination(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, variableItemId); + return definition != null && definition.hasValue() && !definition.isReadOnly(); + } + + private boolean isValidUserCustomReference(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getUserVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue(); + } + + private boolean isValidRoomCustomReference(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getRoomVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue(); + } + + private boolean isValidContextCustomReference(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, variableItemId); + return definition != null && definition.hasValue(); + } + + private boolean isValidFurniCustomReference(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getFurniVariableManager().getDefinitionInfo(variableItemId) : null; + return definition != null && definition.hasValue(); + } + + private Integer readUserInternalValue(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.readUserValue(room, roomUnit, key); + } + + private boolean writeUserInternalValue(Room room, RoomUnit roomUnit, String key, int value) { + return WiredInternalVariableSupport.writeUserValue( + room, + roomUnit, + key, + value, + WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION, + false); + } + + private Integer readFurniInternalValue(Room room, HabboItem item, String key) { + return WiredInternalVariableSupport.readFurniValue(room, item, key); + } + + private boolean writeFurniInternalValue(Room room, HabboItem item, String key, int value) { + return WiredInternalVariableSupport.writeFurniValue(room, item, key, value); + } + + private Integer readRoomInternalValue(Room room, String key) { + return WiredInternalVariableSupport.readRoomValue(room, key); + } + + private Integer getUserTeamScore(Room room, Habbo habbo) { + if (room == null || habbo == null || habbo.getHabboInfo().getGamePlayer() == null) return null; + + Game game = this.resolveTeamGame(room, habbo); + GamePlayer gamePlayer = habbo.getHabboInfo().getGamePlayer(); + + if (game == null || gamePlayer.getTeamColor() == null) return gamePlayer.getScore(); + + GameTeam team = game.getTeam(gamePlayer.getTeamColor()); + return (team != null) ? team.getTotalScore() : gamePlayer.getScore(); + } + + private Integer getTeamColorId(int effectId) { + TeamEffectData data = this.getTeamEffectData(effectId); + return data == null ? null : data.colorId; + } + + private Integer getTeamTypeId(int effectId) { + TeamEffectData data = this.getTeamEffectData(effectId); + return data == null ? null : data.typeId; + } + + private int getTeamMetric(Room room, GameTeamColors color, boolean score) { + Game game = this.resolveTeamGame(room, null); + if (game == null || color == null) return 0; + + GameTeam team = game.getTeam(color); + if (team == null) return 0; + + return score ? team.getTotalScore() : team.getMembers().size(); + } + + private Game resolveTeamGame(Room room, Habbo habbo) { + if (room == null) return null; + + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getCurrentGame() != null) { + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (game != null) return game; + } + + Game wiredGame = room.getGame(WiredGame.class); + if (wiredGame != null) return wiredGame; + + Game freezeGame = room.getGame(FreezeGame.class); + if (freezeGame != null) return freezeGame; + + return room.getGame(BattleBanzaiGame.class); + } + + private TeamEffectData getTeamEffectData(int effectValue) { + if (effectValue <= 0) return null; + + if (effectValue >= 223 && effectValue <= 226) return new TeamEffectData(effectValue - 222, 0); + if (effectValue >= 33 && effectValue <= 36) return new TeamEffectData(effectValue - 32, 1); + if (effectValue >= 40 && effectValue <= 43) return new TeamEffectData(effectValue - 39, 2); + + return null; + } + + private boolean moveUserTo(Room room, RoomUnit roomUnit, int x, int y) { + if (room == null || roomUnit == null || room.getLayout() == null) return false; + + RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); + if (targetTile == null || targetTile.state == RoomTileState.INVALID) return false; + + double targetZ = targetTile.getStackHeight() + ((targetTile.state == RoomTileState.SIT) ? -0.5 : 0); + return WiredUserMovementHelper.moveUser( + room, + roomUnit, + targetTile, + targetZ, + roomUnit.getBodyRotation(), + roomUnit.getHeadRotation(), + WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION, + false); + } + + private boolean moveFurniTo(Room room, HabboItem item, int x, int y, int rotation, double z) { + if (room == null || item == null || room.getLayout() == null) return false; + + RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); + if (targetTile == null || targetTile.state == RoomTileState.INVALID) return false; + + FurnitureMovementError error = room.moveFurniTo(item, targetTile, rotation, z, null, true, true); + return error == FurnitureMovementError.NONE; + } + + private List parseItems(int[] ids, Room room) throws WiredSaveException { + List items = new ArrayList<>(); + if (ids == null || room == null) return items; + + for (int id : ids) { + HabboItem item = room.getHabboItem(id); + if (item == null) throw new WiredSaveException(String.format("Item %s not found", id)); + items.add(item); + } + + return items; + } + + private List parseItems(List ids, Room room) throws WiredSaveException { + List items = new ArrayList<>(); + if (ids == null || room == null) return items; + + for (Integer id : ids) { + if (id == null || id <= 0) continue; + + HabboItem item = room.getHabboItem(id); + if (item == null) throw new WiredSaveException(String.format("Item %s not found", id)); + items.add(item); + } + + return items; + } + + private List parseItems(String ids, Room room) throws WiredSaveException { + List items = new ArrayList<>(); + if (ids == null || ids.trim().isEmpty() || room == null) return items; + + for (String part : ids.split("[;,\\t]")) { + int id = parseInteger(part); + if (id <= 0) continue; + + HabboItem item = room.getHabboItem(id); + if (item == null) throw new WiredSaveException(String.format("Item %s not found", id)); + items.add(item); + } + + return items; + } + + private String serializeStringData() { + return (this.destinationVariableToken == null ? "" : this.destinationVariableToken) + DELIM + (this.referenceVariableToken == null ? "" : this.referenceVariableToken) + DELIM + this.serializeIds(this.referenceSelectedFurni); + } + + private String[] parseStringData(String value) { + return (value == null || value.isEmpty()) ? new String[0] : value.split("\\t", -1); + } + + private List toIds(List items) { + List ids = new ArrayList<>(); + for (HabboItem item : items) if (item != null) ids.add(item.getId()); + return ids; + } + + private String serializeIds(List items) { + StringBuilder builder = new StringBuilder(); + + for (HabboItem item : items) { + if (item == null) continue; + if (builder.length() > 0) builder.append(FURNI_DELIM); + builder.append(item.getId()); + } + + return builder.toString(); + } + + private void setDestinationVariableToken(String token) { + this.destinationVariableToken = normalizeVariableToken(token); + this.destinationVariableItemId = getCustomItemId(this.destinationVariableToken); + } + + private void setReferenceVariableToken(String token) { + this.referenceVariableToken = normalizeVariableToken(token); + this.referenceVariableItemId = getCustomItemId(this.referenceVariableToken); + } + + private static boolean isCustomVariableToken(String token) { + return token != null && token.startsWith(CUSTOM_TOKEN_PREFIX); + } + + private static boolean isInternalVariableToken(String token) { + return token != null && token.startsWith(INTERNAL_TOKEN_PREFIX); + } + + private static int getCustomItemId(String token) { + if (!isCustomVariableToken(token)) return 0; + return parseInteger(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } + + private static String getInternalVariableKey(String token) { + return isInternalVariableToken(token) ? WiredInternalVariableSupport.normalizeKey(token.substring(INTERNAL_TOKEN_PREFIX.length())) : ""; + } + + private static String normalizeVariableToken(String token) { + if (token == null) return ""; + + String normalized = token.trim(); + if (normalized.isEmpty()) return ""; + if (isCustomVariableToken(normalized)) return normalized; + if (isInternalVariableToken(normalized)) return INTERNAL_TOKEN_PREFIX + WiredInternalVariableSupport.normalizeKey(normalized.substring(INTERNAL_TOKEN_PREFIX.length())); + + int parsedValue = parseInteger(normalized); + return parsedValue > 0 ? CUSTOM_TOKEN_PREFIX + parsedValue : ""; + } + + private static boolean canUseUserInternalDestination(String key) { + return WiredInternalVariableSupport.canUseUserDestination(key); + } + + private static boolean canUseFurniInternalDestination(String key) { + return WiredInternalVariableSupport.canUseFurniDestination(key); + } + + private static boolean canUseUserInternalReference(String key) { + return WiredInternalVariableSupport.canUseUserReference(key); + } + + private static boolean canUseFurniInternalReference(String key) { + return WiredInternalVariableSupport.canUseFurniReference(key); + } + + private static boolean canUseRoomInternalReference(String key) { + return WiredInternalVariableSupport.canUseRoomReference(key); + } + + private static boolean canUseContextInternalReference(String key) { + return WiredInternalVariableSupport.canUseContextReference(key); + } + + private static int param(int[] params, int index, int fallback) { + return (params.length > index) ? params[index] : fallback; + } + + private static int normalizeTargetType(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + private static int normalizeReferenceMode(int value) { + return (value == REF_VARIABLE) ? REF_VARIABLE : REF_CONSTANT; + } + + private static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private static int normalizeDestinationFurniSource(int value) { + return switch (value) { + case WiredSourceUtil.SOURCE_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + } + + private static int normalizeReferenceFurniSource(int value) { + return switch (value) { + case SOURCE_SECONDARY_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + } + + private static int normalizeOperation(int value) { + return switch (value) { + case OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_POW, OP_MOD, OP_MIN, OP_MAX, OP_RANDOM, OP_ABS, OP_AND, OP_OR, OP_XOR, OP_NOT, OP_LSHIFT, OP_RSHIFT -> value; + default -> OP_ASSIGN; + }; + } + + private static int parseInteger(String value) { + try { + return (value == null || value.trim().isEmpty()) ? 0 : Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return 0; + } + } + + private static int shift(Integer value) { + return (value == null) ? 0 : Math.max(0, Math.min(31, value)); + } + + private static int clamp(long value) { + return (value > Integer.MAX_VALUE) ? Integer.MAX_VALUE : ((value < Integer.MIN_VALUE) ? Integer.MIN_VALUE : (int) value); + } + + static class JsonData { + int destinationTargetType, destinationVariableItemId, operation, referenceMode, referenceConstantValue, referenceTargetType, referenceVariableItemId, destinationUserSource, destinationFurniSource, referenceUserSource, referenceFurniSource, delay; + String destinationVariableToken, referenceVariableToken; + List destinationSelectedFurniIds, referenceSelectedFurniIds; + + JsonData(int destinationTargetType, String destinationVariableToken, int destinationVariableItemId, int operation, int referenceMode, int referenceConstantValue, int referenceTargetType, String referenceVariableToken, int referenceVariableItemId, int destinationUserSource, int destinationFurniSource, int referenceUserSource, int referenceFurniSource, int delay, List destinationSelectedFurniIds, List referenceSelectedFurniIds) { + this.destinationTargetType = destinationTargetType; + this.destinationVariableToken = destinationVariableToken; + this.destinationVariableItemId = destinationVariableItemId; + this.operation = operation; + this.referenceMode = referenceMode; + this.referenceConstantValue = referenceConstantValue; + this.referenceTargetType = referenceTargetType; + this.referenceVariableToken = referenceVariableToken; + this.referenceVariableItemId = referenceVariableItemId; + this.destinationUserSource = destinationUserSource; + this.destinationFurniSource = destinationFurniSource; + this.referenceUserSource = referenceUserSource; + this.referenceFurniSource = referenceFurniSource; + this.delay = delay; + this.destinationSelectedFurniIds = destinationSelectedFurniIds; + this.referenceSelectedFurniIds = referenceSelectedFurniIds; + } + } + + private static class ReferenceSnapshot { + final int targetType; + final LinkedHashMap values = new LinkedHashMap<>(); + + ReferenceSnapshot(int targetType) { + this.targetType = targetType; + } + + void add(int entityId, int value) { + this.values.put(entityId, value); + } + + boolean isEmpty() { + return this.values.isEmpty(); + } + } + + private static class TeamEffectData { + final int colorId; + final int typeId; + + TeamEffectData(int colorId, int typeId) { + this.colorId = colorId; + this.typeId = typeId; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectControlClock.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectControlClock.java new file mode 100644 index 00000000..b4f55831 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectControlClock.java @@ -0,0 +1,244 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameUpCounter; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class WiredEffectControlClock extends InteractionWiredEffect { + private static final int ACTION_START = 0; + private static final int ACTION_STOP = 1; + private static final int ACTION_RESET = 2; + private static final int ACTION_PAUSE = 3; + private static final int ACTION_RESUME = 4; + + public static final WiredEffectType type = WiredEffectType.CONTROL_CLOCK; + + private final List items = new ArrayList<>(); + private int action = ACTION_START; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredEffectControlClock(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectControlClock(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + + if (room == null) { + return; + } + + List effectiveItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + this.items.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + } + + for (HabboItem item : effectiveItems) { + if (!(item instanceof InteractionGameUpCounter)) { + continue; + } + + InteractionGameUpCounter counter = (InteractionGameUpCounter) item; + + switch (this.action) { + case ACTION_START: + counter.restartFromZero(room); + break; + case ACTION_STOP: + counter.stopCounter(room); + break; + case ACTION_RESET: + counter.resetCounter(room); + break; + case ACTION_PAUSE: + counter.pauseCounter(room); + break; + case ACTION_RESUME: + counter.resumeCounter(room); + break; + } + } + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.getDelay(), + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), + this.action, + this.furniSource + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.items.clear(); + String wiredData = set.getString("wired_data"); + + if (wiredData != null && wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.setDelay(data.delay); + this.action = this.normalizeAction(data.action); + this.furniSource = data.furniSource; + + if (data.itemIds != null) { + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + + if (item != null) { + this.items.add(item); + } + } + } + + return; + } + + this.action = ACTION_START; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.setDelay(0); + } + + @Override + public void onPickUp() { + this.items.clear(); + this.action = ACTION_START; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.setDelay(0); + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + List itemsSnapshot = new ArrayList<>(this.items); + itemsSnapshot.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + + this.items.clear(); + this.items.addAll(itemsSnapshot); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(itemsSnapshot.size()); + for (HabboItem item : itemsSnapshot) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(2); + message.appendInt(this.action); + message.appendInt(this.furniSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + int[] params = settings.getIntParams(); + + if (params.length < 2) { + throw new WiredSaveException("Invalid data"); + } + + this.action = this.normalizeAction(params[0]); + this.furniSource = params[1]; + + int delay = settings.getDelay(); + if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { + throw new WiredSaveException("Delay too long"); + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + + List newItems = new ArrayList<>(); + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item == null) { + throw new WiredSaveException(String.format("Item %s not found", itemId)); + } + + if (!(item instanceof InteractionGameUpCounter)) { + throw new WiredSaveException("wiredfurni.error.require_counter_furni"); + } + + newItems.add(item); + } + + this.items.clear(); + this.items.addAll(newItems); + this.setDelay(delay); + + return true; + } + + private int normalizeAction(int value) { + if (value < ACTION_START || value > ACTION_RESUME) { + return ACTION_START; + } + + return value; + } + + static class JsonData { + int delay; + List itemIds; + int action; + int furniSource; + + public JsonData(int delay, List itemIds, int action, int furniSource) { + this.delay = delay; + this.itemIds = itemIds; + this.action = action; + this.furniSource = furniSource; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFreeze.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFreeze.java new file mode 100644 index 00000000..66f11eae --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFreeze.java @@ -0,0 +1,175 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import gnu.trove.procedure.TObjectProcedure; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class WiredEffectFreeze extends InteractionWiredEffect { + private static final Set ALLOWED_EFFECT_IDS = Set.of(218, 12, 11, 53, 163); + public static final WiredEffectType type = WiredEffectType.FREEZE; + + private int effectId = 218; + private boolean cancelOnTeleport = false; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredEffectFreeze(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectFreeze(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.userSource)) { + if (room.getHabbo(roomUnit) == null) { + continue; + } + + WiredFreezeUtil.freeze(room, roomUnit, this.effectId, this.cancelOnTeleport); + } + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.effectId, this.cancelOnTeleport, this.getDelay(), this.userSource)); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + String wiredData = set.getString("wired_data"); + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.effectId = ALLOWED_EFFECT_IDS.contains(data.effectId) ? data.effectId : 218; + this.cancelOnTeleport = data.cancelOnTeleport; + this.setDelay(data.delay); + this.userSource = data.userSource; + } else { + this.effectId = 218; + this.cancelOnTeleport = false; + this.setDelay(0); + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + } + } + + @Override + public void onPickUp() { + this.effectId = 218; + this.cancelOnTeleport = false; + this.setDelay(0); + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(3); + message.appendInt(this.effectId); + message.appendInt(this.cancelOnTeleport ? 1 : 0); + message.appendInt(this.userSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + + if (this.requiresTriggeringUser()) { + List invalidTriggers = new ArrayList<>(); + room.getRoomSpecialTypes().getTriggers(this.getX(), this.getY()).forEach(new TObjectProcedure() { + @Override + public boolean execute(InteractionWiredTrigger object) { + if (!object.isTriggeredByRoomUnit()) { + invalidTriggers.add(object.getBaseItem().getSpriteId()); + } + return true; + } + }); + message.appendInt(invalidTriggers.size()); + for (Integer i : invalidTriggers) { + message.appendInt(i); + } + } else { + message.appendInt(0); + } + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + if (settings.getIntParams().length < 3) { + throw new WiredSaveException("Invalid data"); + } + + int nextEffectId = settings.getIntParams()[0]; + if (!ALLOWED_EFFECT_IDS.contains(nextEffectId)) { + throw new WiredSaveException("Invalid freeze effect"); + } + + int delay = settings.getDelay(); + if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { + throw new WiredSaveException("Delay too long"); + } + + this.effectId = nextEffectId; + this.cancelOnTeleport = settings.getIntParams()[1] == 1; + this.userSource = settings.getIntParams()[2]; + this.setDelay(delay); + + return true; + } + + @Override + public boolean requiresTriggeringUser() { + return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; + } + + static class JsonData { + int effectId; + boolean cancelOnTeleport; + int delay; + int userSource; + + public JsonData(int effectId, boolean cancelOnTeleport, int delay, int userSource) { + this.effectId = effectId; + this.cancelOnTeleport = cancelOnTeleport; + this.delay = delay; + this.userSource = userSource; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToFurni.java new file mode 100644 index 00000000..ed8bf0a9 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToFurni.java @@ -0,0 +1,360 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.FurnitureMovementError; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class WiredEffectFurniToFurni extends InteractionWiredEffect { + private static final int SOURCE_SECONDARY_SELECTED = 101; + private static final String FURNI_SPLIT_REGEX = "[;,\\t]"; + private static final String FURNI_DELIMITER = ";"; + + public static final WiredEffectType type = WiredEffectType.FURNI_TO_FURNI; + + private final List moveItems = new ArrayList<>(); + private final List targetItems = new ArrayList<>(); + private int moveSource = WiredSourceUtil.SOURCE_TRIGGER; + private int targetSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredEffectFurniToFurni(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectFurniToFurni(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + + if (room == null) { + return; + } + + HabboItem moveItem = this.resolveLastMoveItem(ctx); + HabboItem targetItem = this.resolveLastTargetItem(ctx); + + if (moveItem == null || targetItem == null || moveItem.getId() == targetItem.getId()) { + return; + } + + RoomTile targetTile = room.getLayout().getTile(targetItem.getX(), targetItem.getY()); + if (targetTile == null) { + return; + } + + FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), null, false, ctx); + if (error == FurnitureMovementError.NONE) { + return; + } + + error = WiredMoveCarryHelper.moveFurni(room, this, moveItem, targetTile, moveItem.getRotation(), targetItem.getZ(), null, false, ctx); + if (error == FurnitureMovementError.NONE) { + return; + } + } + + @Deprecated + @Override + public boolean execute(com.eu.habbo.habbohotel.rooms.RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.getDelay(), + this.moveItems.stream().map(HabboItem::getId).collect(Collectors.toList()), + this.targetItems.stream().map(HabboItem::getId).collect(Collectors.toList()), + this.moveSource, + this.targetSource + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.moveItems.clear(); + this.targetItems.clear(); + + String wiredData = set.getString("wired_data"); + + if (wiredData != null && wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + this.setDelay(data.delay); + this.moveSource = data.moveSource; + this.targetSource = this.normalizeTargetSource(data.targetSource); + + this.loadItems(room, data.itemIds, this.moveItems); + this.loadItems(room, data.targetItemIds, this.targetItems); + + if (this.moveSource == WiredSourceUtil.SOURCE_TRIGGER && !this.moveItems.isEmpty()) { + this.moveSource = WiredSourceUtil.SOURCE_SELECTED; + } + + if (this.targetSource == WiredSourceUtil.SOURCE_TRIGGER && !this.targetItems.isEmpty()) { + this.targetSource = SOURCE_SECONDARY_SELECTED; + } + + return; + } + + if (wiredData != null && !wiredData.isEmpty()) { + String[] wiredDataOld = wiredData.split("\t"); + + if (wiredDataOld.length >= 1) { + this.setDelay(Integer.parseInt(wiredDataOld[0])); + } + + if (wiredDataOld.length >= 2 && !wiredDataOld[1].trim().isEmpty()) { + this.loadItems(room, this.parseIds(wiredDataOld[1], room), this.moveItems); + } + } + + this.moveSource = this.moveItems.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; + this.targetSource = this.targetItems.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : SOURCE_SECONDARY_SELECTED; + } + + @Override + public void onPickUp() { + this.moveItems.clear(); + this.targetItems.clear(); + this.moveSource = WiredSourceUtil.SOURCE_TRIGGER; + this.targetSource = WiredSourceUtil.SOURCE_TRIGGER; + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.validateItems(this.moveItems); + this.validateItems(this.targetItems); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(this.moveItems.size()); + + for (HabboItem item : this.moveItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.serializeIds(this.targetItems)); + message.appendInt(2); + message.appendInt(this.moveSource); + message.appendInt(this.targetSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + this.moveSource = (settings.getIntParams().length > 0) ? settings.getIntParams()[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.targetSource = this.normalizeTargetSource((settings.getIntParams().length > 1) ? settings.getIntParams()[1] : WiredSourceUtil.SOURCE_TRIGGER); + + Room room = this.getRoom(); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + List newMoveItems = new ArrayList<>(); + if (this.moveSource == WiredSourceUtil.SOURCE_SELECTED) { + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item == null) { + throw new WiredSaveException(String.format("Item %s not found", itemId)); + } + + newMoveItems.add(item); + } + } + + if (newMoveItems.size() > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + + List newTargetItems = new ArrayList<>(); + if (this.targetSource == SOURCE_SECONDARY_SELECTED) { + newTargetItems.addAll(this.parseItems(settings.getStringParam(), room)); + } + + if (newTargetItems.size() > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + + int delay = settings.getDelay(); + if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { + throw new WiredSaveException("Delay too long"); + } + + this.moveItems.clear(); + this.moveItems.addAll(newMoveItems); + + this.targetItems.clear(); + this.targetItems.addAll(newTargetItems); + + this.setDelay(delay); + + return true; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + protected long requiredCooldown() { + return COOLDOWN_MOVEMENT; + } + + private HabboItem resolveLastMoveItem(WiredContext ctx) { + return this.resolveLastItem(ctx, this.moveSource, this.moveItems); + } + + private HabboItem resolveLastTargetItem(WiredContext ctx) { + int source = (this.targetSource == SOURCE_SECONDARY_SELECTED) ? WiredSourceUtil.SOURCE_SELECTED : this.targetSource; + return this.resolveLastItem(ctx, source, this.targetItems); + } + + private HabboItem resolveLastItem(WiredContext ctx, int source, List items) { + if (source == WiredSourceUtil.SOURCE_SELECTED) { + this.validateItems(items); + } + + List resolvedItems = WiredSourceUtil.resolveItems(ctx, source, items); + + if (resolvedItems.isEmpty()) { + return null; + } + + for (int index = resolvedItems.size() - 1; index >= 0; index--) { + HabboItem item = resolvedItems.get(index); + + if (item != null) { + return item; + } + } + + return null; + } + + private List parseItems(String data, Room room) throws WiredSaveException { + List items = new ArrayList<>(); + if (data == null || data.trim().isEmpty() || room == null) { + return items; + } + + Set seen = new HashSet<>(); + + for (String part : data.split(FURNI_SPLIT_REGEX)) { + if (part == null) { + continue; + } + + String trimmed = part.trim(); + if (trimmed.isEmpty()) { + continue; + } + + int itemId; + try { + itemId = Integer.parseInt(trimmed); + } catch (NumberFormatException e) { + continue; + } + + if (itemId <= 0 || !seen.add(itemId)) { + continue; + } + + HabboItem item = room.getHabboItem(itemId); + if (item == null) { + throw new WiredSaveException(String.format("Item %s not found", itemId)); + } + + items.add(item); + } + + return items; + } + + private List parseIds(String data, Room room) { + try { + return this.parseItems(data, room).stream().map(HabboItem::getId).collect(Collectors.toList()); + } catch (WiredSaveException e) { + return new ArrayList<>(); + } + } + + private void loadItems(Room room, List itemIds, List target) { + if (room == null || itemIds == null || itemIds.isEmpty()) { + return; + } + + for (Integer itemId : itemIds) { + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + target.add(item); + } + } + } + + private int normalizeTargetSource(int source) { + return (source == WiredSourceUtil.SOURCE_SELECTED) ? SOURCE_SECONDARY_SELECTED : source; + } + + private String serializeIds(List items) { + if (items == null || items.isEmpty()) { + return ""; + } + + return items.stream() + .map(HabboItem::getId) + .distinct() + .map(String::valueOf) + .collect(Collectors.joining(FURNI_DELIMITER)); + } + + static class JsonData { + int delay; + List itemIds; + List targetItemIds; + int moveSource; + int targetSource; + + public JsonData(int delay, List itemIds, List targetItemIds, int moveSource, int targetSource) { + this.delay = delay; + this.itemIds = itemIds; + this.targetItemIds = targetItemIds; + this.moveSource = moveSource; + this.targetSource = targetSource; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToUser.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToUser.java new file mode 100644 index 00000000..63274e00 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectFurniToUser.java @@ -0,0 +1,149 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.rooms.FurnitureMovementError; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; +import com.eu.habbo.messages.outgoing.rooms.WiredMovementsComposer; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class WiredEffectFurniToUser extends WiredEffectUserFurniBase { + public static final WiredEffectType type = WiredEffectType.FURNI_TO_USER; + + public WiredEffectFurniToUser(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectFurniToUser(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + List items = new ArrayList<>(this.resolveItems(ctx)); + Habbo habbo = this.resolveLastHabbo(room, ctx); + + if (room == null || habbo == null || habbo.getRoomUnit() == null) { + return; + } + + items.removeIf(item -> item == null); + + if (items.isEmpty()) { + return; + } + + items.sort(Comparator + .comparingDouble(HabboItem::getZ) + .thenComparingInt(HabboItem::getId)); + + Map followerZOverrides = new HashMap<>(); + + for (HabboItem item : items) { + followerZOverrides.put(item.getId(), item.getZ()); + } + + RoomUnit roomUnit = habbo.getRoomUnit(); + boolean hasActiveMoveStatus = roomUnit.hasStatus(RoomUnitStatus.MOVE); + long moveStatusTimestamp = hasActiveMoveStatus ? roomUnit.getMoveStatusTimestamp() : 0L; + + if (roomUnit.isWalking()) { + for (HabboItem item : items) { + if (item == null) { + continue; + } + + WiredMoveCarryHelper.registerUserFollower(room, this, item, roomUnit, followerZOverrides.get(item.getId()), ctx); + } + + if (!hasActiveMoveStatus) { + return; + } + } + + RoomTile targetTile = this.resolveTargetTile(habbo); + if (targetTile == null) { + return; + } + + Integer animationDurationOverride = WiredMoveCarryHelper.hasNoAnimationExtra(room, this) + ? null + : this.resolveFollowAnimationDuration(room, habbo, this); + int anchorType = hasActiveMoveStatus ? WiredMovementsComposer.FURNI_ANCHOR_USER : WiredMovementsComposer.FURNI_ANCHOR_NONE; + int anchorId = hasActiveMoveStatus ? roomUnit.getId() : 0; + + if (hasActiveMoveStatus) { + int animationDuration = WiredMoveCarryHelper.resolveMoveStepDuration(roomUnit); + int animationElapsed = WiredMoveCarryHelper.resolveMoveStepElapsed(roomUnit); + + for (HabboItem item : items) { + if (item == null || WiredMoveCarryHelper.isUserFollowerProcessed(roomUnit, item, moveStatusTimestamp)) { + continue; + } + + Double targetZ = WiredMoveCarryHelper.resolveFollowerStackZ(room, item, targetTile, item.getRotation()); + FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, item, targetTile, item.getRotation(), targetZ, null, false, ctx, animationDuration, animationElapsed, WiredMovementsComposer.FURNI_ANCHOR_USER, roomUnit.getId()); + if (error != FurnitureMovementError.NONE) { + Double fallbackZ = followerZOverrides.get(item.getId()); + + if (fallbackZ != null) { + error = WiredMoveCarryHelper.moveFurni(room, this, item, targetTile, item.getRotation(), fallbackZ, null, false, ctx, animationDuration, animationElapsed, WiredMovementsComposer.FURNI_ANCHOR_USER, roomUnit.getId()); + } + } + + if (error == FurnitureMovementError.NONE) { + WiredMoveCarryHelper.markUserFollowerProcessed(roomUnit, item, moveStatusTimestamp); + } + } + } + + for (HabboItem item : items) { + if (item == null) { + continue; + } + + if (hasActiveMoveStatus && WiredMoveCarryHelper.isUserFollowerProcessed(roomUnit, item, moveStatusTimestamp)) { + continue; + } + + Double targetZ = WiredMoveCarryHelper.resolveFollowerStackZ(room, item, targetTile, item.getRotation()); + FurnitureMovementError error = WiredMoveCarryHelper.moveFurni(room, this, item, targetTile, item.getRotation(), targetZ, null, false, ctx, animationDurationOverride, null, anchorType, anchorId); + if (error == FurnitureMovementError.NONE) { + continue; + } + + Double fallbackZ = followerZOverrides.get(item.getId()); + + if (fallbackZ != null) { + WiredMoveCarryHelper.moveFurni(room, this, item, targetTile, item.getRotation(), fallbackZ, null, false, ctx, animationDurationOverride, null, anchorType, anchorId); + } + } + } + + @Deprecated + @Override + public boolean execute(com.eu.habbo.habbohotel.rooms.RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public WiredEffectType getType() { + return type; + } + +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveHandItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveHandItem.java index b8d1ceaa..e249a9ac 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveHandItem.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveHandItem.java @@ -20,7 +20,7 @@ public class WiredEffectGiveHandItem extends WiredEffectWhisper { @Override public void execute(WiredContext ctx) { try { - int itemId = Integer.parseInt(this.message); + int itemId = Math.max(0, Integer.parseInt(this.message)); Room room = ctx.room(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveReward.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveReward.java index a2df0c12..e57bc582 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveReward.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveReward.java @@ -140,15 +140,6 @@ public class WiredEffectGiveReward extends InteractionWiredEffect { return type; } - @Override - public void onClick(GameClient client, Room room, Object[] objects) throws Exception { - super.onClick(client, room, objects); - - if (client.getHabbo().hasPermission(Permission.ACC_SUPERWIRED)) { - client.getHabbo().whisper(Emulator.getTexts().getValue("hotel.wired.superwired.info"), RoomChatMessageBubbles.BOT); - } - } - @Override public void serializeWiredData(ServerMessage message, Room room) { message.appendBoolean(false); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveScore.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveScore.java index b2b07b4d..546f721d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveScore.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveScore.java @@ -16,27 +16,22 @@ import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; -import gnu.trove.iterator.TObjectIntIterator; -import gnu.trove.map.TObjectIntMap; -import gnu.trove.map.hash.TObjectIntHashMap; import gnu.trove.procedure.TObjectProcedure; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; -import java.util.Map; public class WiredEffectGiveScore extends InteractionWiredEffect { + private static final int OPERATION_ADD = 0; + private static final int OPERATION_REMOVE = 1; public static final WiredEffectType type = WiredEffectType.GIVE_SCORE; private int score; - private int count; + private int operation = OPERATION_ADD; private int userSource = WiredSourceUtil.SOURCE_TRIGGER; - private TObjectIntMap> data = new TObjectIntHashMap<>(); - public WiredEffectGiveScore(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); } @@ -58,45 +53,8 @@ public class WiredEffectGiveScore extends InteractionWiredEffect { if (game == null) continue; - int gameStartTime = game.getStartTime(); - - TObjectIntMap> dataClone = new TObjectIntHashMap<>(this.data); - - TObjectIntIterator> iterator = dataClone.iterator(); - - boolean alreadyCounted = false; - for (int i = dataClone.size(); i-- > 0; ) { - iterator.advance(); - - Map.Entry map = iterator.key(); - - if (map.getValue() == habbo.getHabboInfo().getId()) { - if (map.getKey() == gameStartTime) { - if (iterator.value() < this.count) { - iterator.setValue(iterator.value() + 1); - - habbo.getHabboInfo().getGamePlayer().addScore(this.score, true); - - alreadyCounted = true; - break; - } - } else { - iterator.remove(); - } - } - } - - if (!alreadyCounted) { - try { - this.data.put(new AbstractMap.SimpleEntry<>(gameStartTime, habbo.getHabboInfo().getId()), 1); - } - catch(IllegalArgumentException e) { - - } - - if (habbo.getHabboInfo().getGamePlayer() != null) { - habbo.getHabboInfo().getGamePlayer().addScore(this.score, true); - } + if (habbo.getHabboInfo().getGamePlayer() != null) { + habbo.getHabboInfo().getGamePlayer().addScore(this.getAppliedAmount(), true); } } } @@ -109,7 +67,7 @@ public class WiredEffectGiveScore extends InteractionWiredEffect { @Override public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData(this.score, this.count, this.getDelay(), this.userSource)); + return WiredManager.getGson().toJson(new JsonData(this.score, this.operation, this.getDelay(), this.userSource)); } @Override @@ -119,7 +77,7 @@ public class WiredEffectGiveScore extends InteractionWiredEffect { if(wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.score = data.score; - this.count = data.count; + this.operation = this.normalizeOperation(data.operation); this.setDelay(data.delay); this.userSource = data.userSource; } @@ -128,7 +86,7 @@ public class WiredEffectGiveScore extends InteractionWiredEffect { if (data.length == 3) { this.score = Integer.parseInt(data[0]); - this.count = Integer.parseInt(data[1]); + this.operation = OPERATION_ADD; this.setDelay(Integer.parseInt(data[2])); } @@ -140,7 +98,7 @@ public class WiredEffectGiveScore extends InteractionWiredEffect { @Override public void onPickUp() { this.score = 0; - this.count = 0; + this.operation = OPERATION_ADD; this.setDelay(0); this.userSource = WiredSourceUtil.SOURCE_TRIGGER; } @@ -160,7 +118,7 @@ public class WiredEffectGiveScore extends InteractionWiredEffect { message.appendString(""); message.appendInt(3); message.appendInt(this.score); - message.appendInt(this.count); + message.appendInt(this.operation); message.appendInt(this.userSource); message.appendInt(0); message.appendInt(this.getType().code); @@ -195,10 +153,7 @@ public class WiredEffectGiveScore extends InteractionWiredEffect { if(score < 1 || score > 100) throw new WiredSaveException("Score is invalid"); - int timesPerGame = settings.getIntParams()[1]; - - if(timesPerGame < 1 || timesPerGame > 10) - throw new WiredSaveException("Times per game is invalid"); + int operation = this.normalizeOperation(settings.getIntParams()[1]); this.userSource = settings.getIntParams()[2]; int delay = settings.getDelay(); @@ -207,7 +162,7 @@ public class WiredEffectGiveScore extends InteractionWiredEffect { throw new WiredSaveException("Delay too long"); this.score = score; - this.count = timesPerGame; + this.operation = operation; this.setDelay(delay); return true; @@ -218,15 +173,23 @@ public class WiredEffectGiveScore extends InteractionWiredEffect { return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; } + private int normalizeOperation(int value) { + return (value == OPERATION_REMOVE) ? OPERATION_REMOVE : OPERATION_ADD; + } + + private int getAppliedAmount() { + return (this.operation == OPERATION_REMOVE) ? -this.score : this.score; + } + static class JsonData { int score; - int count; + int operation; int delay; int userSource; - public JsonData(int score, int count, int delay, int userSource) { + public JsonData(int score, int operation, int delay, int userSource) { this.score = score; - this.count = count; + this.operation = operation; this.delay = delay; this.userSource = userSource; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveScoreToTeam.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveScoreToTeam.java index 7b2c0efd..e9ff423a 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveScoreToTeam.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveScoreToTeam.java @@ -16,20 +16,19 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; -import gnu.trove.map.hash.TIntIntHashMap; import java.sql.ResultSet; import java.sql.SQLException; public class WiredEffectGiveScoreToTeam extends InteractionWiredEffect { + private static final int OPERATION_ADD = 0; + private static final int OPERATION_REMOVE = 1; public static final WiredEffectType type = WiredEffectType.GIVE_SCORE_TEAM; private int points; - private int count; + private int operation = OPERATION_ADD; private GameTeamColors teamColor = GameTeamColors.RED; - private TIntIntHashMap startTimes = new TIntIntHashMap(); - public WiredEffectGiveScoreToTeam(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { super(id, userId, item, extradata, limitedStack, limitedSells); } @@ -43,16 +42,10 @@ public class WiredEffectGiveScoreToTeam extends InteractionWiredEffect { Room room = ctx.room(); for (Game game : room.getGames()) { if (game != null && game.state.equals(GameState.RUNNING)) { - int c = this.startTimes.get(game.getStartTime()); + GameTeam team = game.getTeam(this.teamColor); - if (c < this.count) { - GameTeam team = game.getTeam(this.teamColor); - - if (team != null) { - team.addTeamScore(this.points); - - this.startTimes.put(game.getStartTime(), c + 1); - } + if (team != null) { + team.addTeamScore(this.getAppliedAmount(team)); } } } @@ -66,7 +59,7 @@ public class WiredEffectGiveScoreToTeam extends InteractionWiredEffect { @Override public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData(this.points, this.count, this.teamColor, this.getDelay())); + return WiredManager.getGson().toJson(new JsonData(this.points, this.operation, this.teamColor, this.getDelay())); } @Override @@ -76,7 +69,7 @@ public class WiredEffectGiveScoreToTeam extends InteractionWiredEffect { if(wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.points = data.score; - this.count = data.count; + this.operation = this.normalizeOperation(data.operation); this.teamColor = data.team; this.setDelay(data.delay); } @@ -85,8 +78,8 @@ public class WiredEffectGiveScoreToTeam extends InteractionWiredEffect { if (data.length == 4) { this.points = Integer.parseInt(data[0]); - this.count = Integer.parseInt(data[1]); - this.teamColor = GameTeamColors.values()[Integer.parseInt(data[2])]; + this.operation = OPERATION_ADD; + this.teamColor = GameTeamColors.fromType(Integer.parseInt(data[2])); this.setDelay(Integer.parseInt(data[3])); } @@ -96,9 +89,8 @@ public class WiredEffectGiveScoreToTeam extends InteractionWiredEffect { @Override public void onPickUp() { - this.startTimes.clear(); this.points = 0; - this.count = 0; + this.operation = OPERATION_ADD; this.teamColor = GameTeamColors.RED; this.setDelay(0); } @@ -118,7 +110,7 @@ public class WiredEffectGiveScoreToTeam extends InteractionWiredEffect { message.appendString(""); message.appendInt(3); message.appendInt(this.points); - message.appendInt(this.count); + message.appendInt(this.operation); message.appendInt(this.teamColor.type); message.appendInt(0); message.appendInt(this.getType().code); @@ -135,10 +127,7 @@ public class WiredEffectGiveScoreToTeam extends InteractionWiredEffect { if(points < 1 || points > 100) throw new WiredSaveException("Points is invalid"); - int timesPerGame = settings.getIntParams()[1]; - - if(timesPerGame < 1 || timesPerGame > 10) - throw new WiredSaveException("Times per game is invalid"); + int operation = this.normalizeOperation(settings.getIntParams()[1]); int team = settings.getIntParams()[2]; @@ -151,22 +140,34 @@ public class WiredEffectGiveScoreToTeam extends InteractionWiredEffect { throw new WiredSaveException("Delay too long"); this.points = points; - this.count = timesPerGame; - this.teamColor = GameTeamColors.values()[team]; + this.operation = operation; + this.teamColor = GameTeamColors.fromType(team); this.setDelay(delay); return true; } + private int normalizeOperation(int value) { + return (value == OPERATION_REMOVE) ? OPERATION_REMOVE : OPERATION_ADD; + } + + private int getAppliedAmount(GameTeam team) { + if (this.operation != OPERATION_REMOVE) { + return this.points; + } + + return -Math.min(this.points, team.getTeamScore()); + } + static class JsonData { int score; - int count; + int operation; GameTeamColors team; int delay; - public JsonData(int score, int count, GameTeamColors team, int delay) { + public JsonData(int score, int operation, GameTeamColors team, int delay) { this.score = score; - this.count = count; + this.operation = operation; this.team = team; this.delay = delay; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveVariable.java new file mode 100644 index 00000000..73c26bba --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveVariable.java @@ -0,0 +1,434 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import gnu.trove.set.hash.THashSet; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredEffectGiveVariable extends InteractionWiredEffect { + public static final WiredEffectType type = WiredEffectType.GIVE_VAR; + + public static final int TARGET_USER = 0; + public static final int TARGET_FURNI = 1; + public static final int TARGET_CONTEXT = 2; + + private int variableItemId = 0; + private int targetType = TARGET_USER; + private boolean overrideExisting = false; + private int initialValue = 0; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private final THashSet selectedFurni; + + public WiredEffectGiveVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + this.selectedFurni = new THashSet<>(); + } + + public WiredEffectGiveVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + this.selectedFurni = new THashSet<>(); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + + if (room == null) { + return; + } + + switch (this.targetType) { + case TARGET_USER: + this.executeUserVariables(ctx, room); + return; + case TARGET_FURNI: + this.executeFurniVariables(ctx, room); + return; + case TARGET_CONTEXT: + this.executeContextVariables(ctx, room); + return; + default: + return; + } + } + + private void executeContextVariables(WiredContext ctx, Room room) { + WiredVariableDefinitionInfo definitionInfo = WiredContextVariableSupport.getDefinitionInfo(room, this.variableItemId); + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return; + } + + Integer value = definitionInfo.hasValue() ? this.initialValue : null; + WiredContextVariableSupport.assignVariable(ctx, room, this.variableItemId, value, this.overrideExisting); + } + + private void executeUserVariables(WiredContext ctx, Room room) { + WiredVariableDefinitionInfo definitionInfo = room.getUserVariableManager().getDefinitionInfo(this.variableItemId); + + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return; + } + + List users = WiredSourceUtil.resolveUsers(ctx, this.userSource); + + if (users.isEmpty()) { + return; + } + + Integer value = definitionInfo.hasValue() ? this.initialValue : null; + + for (RoomUnit roomUnit : users) { + if (roomUnit == null) { + continue; + } + + Habbo habbo = room.getHabbo(roomUnit); + + if (habbo == null) { + continue; + } + + room.getUserVariableManager().assignVariable(habbo, this.variableItemId, value, this.overrideExisting); + } + } + + private void executeFurniVariables(WiredContext ctx, Room room) { + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.variableItemId); + + if (definition == null || definition.isReadOnly()) { + return; + } + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + this.validateItems(this.selectedFurni); + } + + List furni = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.selectedFurni); + + if (furni.isEmpty()) { + return; + } + + Integer value = definition.hasValue() ? this.initialValue : null; + + for (HabboItem item : furni) { + if (item == null) { + continue; + } + + room.getFurniVariableManager().assignVariable(item, this.variableItemId, value, this.overrideExisting); + } + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + List selectedItems = new ArrayList<>(); + + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + for (HabboItem item : this.selectedFurni) { + if (item != null && room != null && room.getHabboItem(item.getId()) != null) { + selectedItems.add(item); + } + } + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(selectedItems.size()); + + for (HabboItem item : selectedItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(String.valueOf(this.variableItemId)); + message.appendInt(5); + message.appendInt(this.targetType); + message.appendInt(this.overrideExisting ? 1 : 0); + message.appendInt(this.initialValue); + message.appendInt(this.userSource); + message.appendInt(this.furniSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + int[] intParams = settings.getIntParams(); + int nextTargetType = normalizeTargetType((intParams.length > 0) ? intParams[0] : TARGET_USER); + boolean nextOverrideExisting = (intParams.length > 1) && (intParams[1] == 1); + int nextInitialValue = (intParams.length > 2) ? intParams[2] : 0; + int nextUserSource = normalizeUserSource((intParams.length > 3) ? intParams[3] : WiredSourceUtil.SOURCE_TRIGGER); + int nextFurniSource = normalizeFurniSource((intParams.length > 4) ? intParams[4] : WiredSourceUtil.SOURCE_TRIGGER); + int nextVariableItemId = parseVariableItemId(settings.getStringParam()); + + if (nextVariableItemId <= 0 && settings.getFurniIds() != null && settings.getFurniIds().length > 0) { + int legacyItemId = settings.getFurniIds()[0]; + + if (room.getUserVariableManager().hasDefinition(legacyItemId)) { + nextVariableItemId = legacyItemId; + nextTargetType = TARGET_USER; + } + } + + if (nextVariableItemId <= 0) { + throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + } + + WiredVariableDefinitionInfo userDefinitionInfo = (nextTargetType == TARGET_USER) ? room.getUserVariableManager().getDefinitionInfo(nextVariableItemId) : null; + + if (nextTargetType == TARGET_USER && (userDefinitionInfo == null || userDefinitionInfo.isReadOnly())) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + if (nextTargetType == TARGET_FURNI) { + WiredVariableDefinitionInfo furniDefinitionInfo = room.getFurniVariableManager().getDefinitionInfo(nextVariableItemId); + + if (furniDefinitionInfo == null || furniDefinitionInfo.isReadOnly()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + } + + if (nextTargetType == TARGET_CONTEXT) { + WiredVariableDefinitionInfo contextDefinitionInfo = WiredContextVariableSupport.getDefinitionInfo(room, nextVariableItemId); + + if (contextDefinitionInfo == null || contextDefinitionInfo.isReadOnly()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + } + + this.selectedFurni.clear(); + + if (nextTargetType == TARGET_FURNI && nextFurniSource == WiredSourceUtil.SOURCE_SELECTED) { + int[] furniIds = settings.getFurniIds(); + int itemsCount = (furniIds != null) ? furniIds.length : 0; + + if (itemsCount > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + + for (int i = 0; i < itemsCount; i++) { + int itemId = furniIds[i]; + HabboItem item = room.getHabboItem(itemId); + + if (item == null) { + throw new WiredSaveException(String.format("Item %s not found", itemId)); + } + + this.selectedFurni.add(item); + } + } + + this.variableItemId = nextVariableItemId; + this.targetType = nextTargetType; + this.overrideExisting = nextOverrideExisting; + this.initialValue = nextInitialValue; + this.userSource = nextUserSource; + this.furniSource = nextFurniSource; + this.setDelay(settings.getDelay()); + + return true; + } + + @Override + public String getWiredData() { + List selectedItemIds = new ArrayList<>(); + + for (HabboItem item : this.selectedFurni) { + if (item != null) { + selectedItemIds.add(item.getId()); + } + } + + return WiredManager.getGson().toJson(new JsonData(this.variableItemId, this.targetType, this.overrideExisting, this.initialValue, this.userSource, this.furniSource, this.getDelay(), selectedItemIds)); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.variableItemId = Math.max(0, data.variableItemId); + this.targetType = normalizeTargetType(data.targetType); + this.overrideExisting = data.overrideExisting; + this.initialValue = data.initialValue; + this.userSource = normalizeUserSource(data.userSource); + this.furniSource = normalizeFurniSource(data.furniSource); + this.setDelay(Math.max(0, data.delay)); + + if (room != null && data.selectedFurniIds != null) { + for (Integer itemId : data.selectedFurniIds) { + if (itemId == null || itemId <= 0) { + continue; + } + + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + this.selectedFurni.add(item); + } + } + } + } + + return; + } + + try { + this.variableItemId = Math.max(0, Integer.parseInt(wiredData.trim())); + this.targetType = TARGET_USER; + } catch (NumberFormatException ignored) { + } + } + + @Override + public void onPickUp() { + this.variableItemId = 0; + this.targetType = TARGET_USER; + this.overrideExisting = false; + this.initialValue = 0; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.selectedFurni.clear(); + this.setDelay(0); + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public WiredEffectType getType() { + return type; + } + + public int getVariableItemId() { + return this.variableItemId; + } + + public int getTargetType() { + return this.targetType; + } + + public boolean isOverrideExisting() { + return this.overrideExisting; + } + + public int getInitialValue() { + return this.initialValue; + } + + public int getUserSource() { + return this.userSource; + } + + public int getFurniSource() { + return this.furniSource; + } + + public THashSet getSelectedFurni() { + return this.selectedFurni; + } + + private static int normalizeTargetType(int value) { + switch (value) { + case TARGET_FURNI: + case TARGET_CONTEXT: + return value; + default: + return TARGET_USER; + } + } + + private static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private static int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + private static int parseVariableItemId(String value) { + if (value == null || value.trim().isEmpty()) { + return 0; + } + + try { + return Math.max(0, Integer.parseInt(value.trim())); + } catch (NumberFormatException ignored) { + return 0; + } + } + + static class JsonData { + int variableItemId; + int targetType; + boolean overrideExisting; + int initialValue; + int userSource; + int furniSource; + int delay; + List selectedFurniIds; + + JsonData(int variableItemId, int targetType, boolean overrideExisting, int initialValue, int userSource, int furniSource, int delay, List selectedFurniIds) { + this.variableItemId = variableItemId; + this.targetType = targetType; + this.overrideExisting = overrideExisting; + this.initialValue = initialValue; + this.userSource = userSource; + this.furniSource = furniSource; + this.delay = delay; + this.selectedFurniIds = selectedFurniIds; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectJoinTeam.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectJoinTeam.java index 82b94846..525f0fc5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectJoinTeam.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectJoinTeam.java @@ -2,6 +2,8 @@ package com.eu.habbo.habbohotel.items.interactions.wired.effects; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame; +import com.eu.habbo.habbohotel.games.freeze.FreezeGame; import com.eu.habbo.habbohotel.games.Game; import com.eu.habbo.habbohotel.games.GameTeamColors; import com.eu.habbo.habbohotel.games.wired.WiredGame; @@ -26,9 +28,13 @@ import java.util.ArrayList; import java.util.List; public class WiredEffectJoinTeam extends InteractionWiredEffect { + private static final int TEAM_TYPE_WIRED = 0; + private static final int TEAM_TYPE_BANZAI = 1; + private static final int TEAM_TYPE_FREEZE = 2; public static final WiredEffectType type = WiredEffectType.JOIN_TEAM; private GameTeamColors teamColor = GameTeamColors.RED; + private int teamType = TEAM_TYPE_WIRED; private int userSource = WiredSourceUtil.SOURCE_TRIGGER; public WiredEffectJoinTeam(ResultSet set, Item baseItem) throws SQLException { @@ -42,20 +48,30 @@ public class WiredEffectJoinTeam extends InteractionWiredEffect { @Override public void execute(WiredContext ctx) { Room room = ctx.room(); + Class targetGameType = this.resolveGameType(); for (RoomUnit unit : WiredSourceUtil.resolveUsers(ctx, this.userSource)) { Habbo habbo = room.getHabbo(unit); if (habbo == null) continue; - WiredGame game = (WiredGame) room.getGameOrCreate(WiredGame.class); + Game currentGame = null; + if (habbo.getHabboInfo().getCurrentGame() != null) { + currentGame = room.getGame(habbo.getHabboInfo().getCurrentGame()); + } - if (habbo.getHabboInfo().getGamePlayer() != null && habbo.getHabboInfo().getCurrentGame() != null && (habbo.getHabboInfo().getCurrentGame() != WiredGame.class || (habbo.getHabboInfo().getCurrentGame() == WiredGame.class && habbo.getHabboInfo().getGamePlayer().getTeamColor() != this.teamColor))) { - // remove from current game - Game currentGame = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (habbo.getHabboInfo().getGamePlayer() != null + && habbo.getHabboInfo().getCurrentGame() != null + && (habbo.getHabboInfo().getCurrentGame() != targetGameType + || habbo.getHabboInfo().getGamePlayer().getTeamColor() != this.teamColor) + && currentGame != null) { currentGame.removeHabbo(habbo); } if(habbo.getHabboInfo().getGamePlayer() == null) { + Game game = room.getGameOrCreate(targetGameType); + if (game == null) { + continue; + } game.addHabbo(habbo, this.teamColor); } } @@ -69,7 +85,7 @@ public class WiredEffectJoinTeam extends InteractionWiredEffect { @Override public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData(this.teamColor, this.getDelay(), this.userSource)); + return WiredManager.getGson().toJson(new JsonData(this.teamColor, this.teamType, this.getDelay(), this.userSource)); } @Override @@ -80,6 +96,7 @@ public class WiredEffectJoinTeam extends InteractionWiredEffect { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.setDelay(data.delay); this.teamColor = data.team; + this.teamType = this.normalizeTeamType(data.teamType); this.userSource = data.userSource; } else { @@ -89,11 +106,12 @@ public class WiredEffectJoinTeam extends InteractionWiredEffect { this.setDelay(Integer.parseInt(data[0])); if (data.length >= 2) { - this.teamColor = GameTeamColors.values()[Integer.parseInt(data[1])]; + this.teamColor = GameTeamColors.fromType(Integer.parseInt(data[1])); } } this.needsUpdate(true); + this.teamType = TEAM_TYPE_WIRED; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; } } @@ -101,6 +119,7 @@ public class WiredEffectJoinTeam extends InteractionWiredEffect { @Override public void onPickUp() { this.teamColor = GameTeamColors.RED; + this.teamType = TEAM_TYPE_WIRED; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; this.setDelay(0); } @@ -118,7 +137,8 @@ public class WiredEffectJoinTeam extends InteractionWiredEffect { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(2); + message.appendInt(3); + message.appendInt(this.teamType); message.appendInt(this.teamColor.type); message.appendInt(this.userSource); message.appendInt(0); @@ -149,8 +169,15 @@ public class WiredEffectJoinTeam extends InteractionWiredEffect { public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { if(settings.getIntParams().length < 2) throw new WiredSaveException("invalid data"); - int team = settings.getIntParams()[0]; - this.userSource = settings.getIntParams()[1]; + if (settings.getIntParams().length > 2) { + this.teamType = this.normalizeTeamType(settings.getIntParams()[0]); + this.userSource = settings.getIntParams()[2]; + } else { + this.teamType = TEAM_TYPE_WIRED; + this.userSource = settings.getIntParams()[1]; + } + + int team = (settings.getIntParams().length > 2) ? settings.getIntParams()[1] : settings.getIntParams()[0]; if(team < 1 || team > 4) throw new WiredSaveException("Team is invalid"); @@ -160,7 +187,7 @@ public class WiredEffectJoinTeam extends InteractionWiredEffect { if(delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) throw new WiredSaveException("Delay too long"); - this.teamColor = GameTeamColors.values()[team]; + this.teamColor = GameTeamColors.fromType(team); this.setDelay(delay); return true; @@ -171,13 +198,34 @@ public class WiredEffectJoinTeam extends InteractionWiredEffect { return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; } + private int normalizeTeamType(int value) { + if (value == TEAM_TYPE_BANZAI || value == TEAM_TYPE_FREEZE) { + return value; + } + + return TEAM_TYPE_WIRED; + } + + private Class resolveGameType() { + switch (this.teamType) { + case TEAM_TYPE_BANZAI: + return BattleBanzaiGame.class; + case TEAM_TYPE_FREEZE: + return FreezeGame.class; + default: + return WiredGame.class; + } + } + static class JsonData { GameTeamColors team; + int teamType; int delay; int userSource; - public JsonData(GameTeamColors team, int delay, int userSource) { + public JsonData(GameTeamColors team, int teamType, int delay, int userSource) { this.team = team; + this.teamType = teamType; this.delay = delay; this.userSource = userSource; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectKickHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectKickHabbo.java index 5df7a21f..1803652f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectKickHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectKickHabbo.java @@ -16,6 +16,7 @@ import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTextPlaceholderUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserWhisperComposer; @@ -70,8 +71,10 @@ public class WiredEffectKickHabbo extends InteractionWiredEffect { room.giveEffect(habbo, 4, 2); - if (!this.message.isEmpty()) - habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(this.message, habbo, habbo, RoomChatMessageBubbles.ALERT))); + if (!this.message.isEmpty()) { + String message = WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, this.message); + habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(message, habbo, habbo, RoomChatMessageBubbles.ALERT))); + } Emulator.getThreading().run(new RoomUnitKick(habbo, room, true), 2000); } @@ -183,7 +186,7 @@ public class WiredEffectKickHabbo extends InteractionWiredEffect { @Override public boolean requiresTriggeringUser() { - return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; + return this.userSource == WiredSourceUtil.SOURCE_TRIGGER || WiredTextPlaceholderUtil.requiresActor(this.getRoom(), this); } static class JsonData { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMatchFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMatchFurni.java index bab1896a..dd616217 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMatchFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMatchFurni.java @@ -12,10 +12,10 @@ import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.WiredMatchFurniSetting; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; -import com.eu.habbo.messages.outgoing.rooms.items.FloorItemOnRollerComposer; import gnu.trove.set.hash.THashSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,6 +25,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; +import java.math.BigDecimal; public class WiredEffectMatchFurni extends InteractionWiredEffect implements InteractionWiredMatchFurniSettings { private static final Logger LOGGER = LoggerFactory.getLogger(WiredEffectMatchFurni.class); @@ -35,6 +36,7 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int private boolean state = false; private boolean direction = false; private boolean position = false; + private boolean altitude = false; private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; public WiredEffectMatchFurni(ResultSet set, Item baseItem) throws SQLException { @@ -50,6 +52,7 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int @Override public void execute(WiredContext ctx) { Room room = ctx.room(); + this.refresh(); if(this.settings.isEmpty()) return; @@ -57,54 +60,86 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int if (room.getLayout() == null) return; - java.util.Set allowedItemIds = null; - if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { - allowedItemIds = new java.util.HashSet<>(); - for (HabboItem si : WiredSourceUtil.resolveItems(ctx, this.furniSource, null)) { - if (si != null) { - allowedItemIds.add(si.getId()); + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + for (WiredMatchFurniSetting setting : this.settings) { + HabboItem item = room.getHabboItem(setting.item_id); + if (item != null) { + this.applySetting(room, item, setting, ctx); } } - if (allowedItemIds.isEmpty()) { - return; + + return; + } + + List targets = WiredSourceUtil.resolveItems(ctx, this.furniSource, null); + if (targets.isEmpty()) { + return; + } + + for (HabboItem item : targets) { + if (item == null) continue; + + WiredMatchFurniSetting setting = this.resolveSettingForTarget(room, item); + if (setting == null) continue; + + this.applySetting(room, item, setting, ctx); + } + } + + private WiredMatchFurniSetting resolveSettingForTarget(Room room, HabboItem target) { + WiredMatchFurniSetting fallback = null; + + for (WiredMatchFurniSetting setting : this.settings) { + HabboItem sourceItem = room.getHabboItem(setting.item_id); + if (sourceItem == null) continue; + if (sourceItem.getBaseItem().getId() != target.getBaseItem().getId()) continue; + + if (setting.state.equals(target.getExtradata())) { + return setting; + } + + if (fallback == null) { + fallback = setting; } } - for (WiredMatchFurniSetting setting : this.settings) { - if (allowedItemIds != null && !allowedItemIds.contains(setting.item_id)) continue; + return fallback; + } - HabboItem item = room.getHabboItem(setting.item_id); - if (item != null) { - if (this.state && (this.checkForWiredResetPermission && item.allowWiredResetState())) { - if (!setting.state.equals(" ") && !item.getExtradata().equals(setting.state)) { - item.setExtradata(setting.state); - room.updateItemState(item); - } + private void applySetting(Room room, HabboItem item, WiredMatchFurniSetting setting, WiredContext ctx) { + if (this.state && (this.checkForWiredResetPermission && item.allowWiredResetState())) { + if (!setting.state.equals(" ") && !item.getExtradata().equals(setting.state)) { + item.setExtradata(setting.state); + item.needsUpdate(true); + room.updateItemState(item); + } + } + + RoomTile oldLocation = room.getLayout().getTile(item.getX(), item.getY()); + if (oldLocation == null) return; + + if(this.direction && !this.position) { + if(item.getRotation() != setting.rotation && room.furnitureFitsAt(oldLocation, item, setting.rotation, false) == FurnitureMovementError.NONE) { + WiredMoveCarryHelper.moveFurni(room, this, item, oldLocation, setting.rotation, null, true, ctx); + } + } + else if(this.altitude && !this.position) { + int newRotation = this.direction ? setting.rotation : item.getRotation(); + if(BigDecimal.valueOf(item.getZ()).compareTo(BigDecimal.valueOf(setting.z)) != 0 || newRotation != item.getRotation()) { + WiredMoveCarryHelper.moveFurni(room, this, item, oldLocation, newRotation, setting.z, null, true, ctx); + } + } + else if(this.position) { + boolean slideAnimation = !this.direction || item.getRotation() == setting.rotation; + RoomTile newLocation = room.getLayout().getTile((short) setting.x, (short) setting.y); + int newRotation = this.direction ? setting.rotation : item.getRotation(); + double newZ = this.altitude ? setting.z : item.getZ(); + + if (newLocation != null && newLocation.state != RoomTileState.INVALID + && (newLocation != oldLocation || newRotation != item.getRotation()) + && WiredMoveCarryHelper.getMovementError(room, this, item, newLocation, newRotation, ctx) == FurnitureMovementError.NONE) { + if (WiredMoveCarryHelper.moveFurni(room, this, item, newLocation, newRotation, newZ, null, !slideAnimation, ctx) == FurnitureMovementError.NONE) { } - - RoomTile oldLocation = room.getLayout().getTile(item.getX(), item.getY()); - if (oldLocation == null) continue; - double oldZ = item.getZ(); - - if(this.direction && !this.position) { - if(item.getRotation() != setting.rotation && room.furnitureFitsAt(oldLocation, item, setting.rotation, false) == FurnitureMovementError.NONE) { - room.moveFurniTo(item, oldLocation, setting.rotation, null, true); - } - } - else if(this.position) { - boolean slideAnimation = !this.direction || item.getRotation() == setting.rotation; - RoomTile newLocation = room.getLayout().getTile((short) setting.x, (short) setting.y); - int newRotation = this.direction ? setting.rotation : item.getRotation(); - - if(newLocation != null && newLocation.state != RoomTileState.INVALID && (newLocation != oldLocation || newRotation != item.getRotation()) && room.furnitureFitsAt(newLocation, item, newRotation, true) == FurnitureMovementError.NONE) { - if(room.moveFurniTo(item, newLocation, newRotation, null, !slideAnimation) == FurnitureMovementError.NONE) { - if(slideAnimation) { - room.sendComposer(new FloorItemOnRollerComposer(item, null, oldLocation, oldZ, newLocation, item.getZ(), 0, room).compose()); - } - } - } - } - } } } @@ -118,7 +153,7 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int @Override public String getWiredData() { this.refresh(); - return WiredManager.getGson().toJson(new JsonData(this.state, this.direction, this.position, new ArrayList(this.settings), this.getDelay(), this.furniSource)); + return WiredManager.getGson().toJson(new JsonData(this.state, this.direction, this.position, this.altitude, new ArrayList(this.settings), this.getDelay(), this.furniSource)); } @Override @@ -131,12 +166,10 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int this.state = data.state; this.direction = data.direction; this.position = data.position; + this.altitude = data.altitude; this.settings.clear(); this.settings.addAll(data.items); this.furniSource = data.furniSource; - if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.settings.isEmpty()) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; - } } else { String[] data = set.getString("wired_data").split(":"); @@ -150,7 +183,9 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int String[] stuff = items[i].split(Pattern.quote("-")); - if (stuff.length >= 5) { + if (stuff.length >= 6) { + this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]), Double.parseDouble(stuff[5]))); + } else if (stuff.length >= 5) { this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]))); } @@ -162,6 +197,7 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int this.state = data[2].equals("1"); this.direction = data[3].equals("1"); this.position = data[4].equals("1"); + this.altitude = false; this.setDelay(Integer.parseInt(data[5])); this.furniSource = this.settings.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; this.needsUpdate(true); @@ -174,6 +210,7 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int this.state = false; this.direction = false; this.position = false; + this.altitude = false; this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; this.setDelay(0); } @@ -197,10 +234,11 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(4); + message.appendInt(5); message.appendInt(this.state ? 1 : 0); message.appendInt(this.direction ? 1 : 0); message.appendInt(this.position ? 1 : 0); + message.appendInt(this.altitude ? 1 : 0); message.appendInt(this.furniSource); message.appendInt(0); message.appendInt(this.getType().code); @@ -214,30 +252,30 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int boolean setState = settings.getIntParams()[0] == 1; boolean setDirection = settings.getIntParams()[1] == 1; boolean setPosition = settings.getIntParams()[2] == 1; - this.furniSource = settings.getIntParams()[3]; + boolean setAltitude = (settings.getIntParams().length > 4) ? (settings.getIntParams()[3] == 1) : false; + this.furniSource = (settings.getIntParams().length > 4) ? settings.getIntParams()[4] : settings.getIntParams()[3]; Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); if (room == null) throw new WiredSaveException("Trying to save wired in unloaded room"); + int itemsCount = settings.getFurniIds().length; + + if(itemsCount > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + List newSettings = new ArrayList<>(); - if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { - int itemsCount = settings.getFurniIds().length; - if(itemsCount > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { - throw new WiredSaveException("Too many furni selected"); - } + for (int i = 0; i < itemsCount; i++) { + int itemId = settings.getFurniIds()[i]; + HabboItem it = room.getHabboItem(itemId); - for (int i = 0; i < itemsCount; i++) { - int itemId = settings.getFurniIds()[i]; - HabboItem it = room.getHabboItem(itemId); + if(it == null) + throw new WiredSaveException(String.format("Item %s not found", itemId)); - if(it == null) - throw new WiredSaveException(String.format("Item %s not found", itemId)); - - newSettings.add(new WiredMatchFurniSetting(it.getId(), this.checkForWiredResetPermission && it.allowWiredResetState() ? it.getExtradata() : " ", it.getRotation(), it.getX(), it.getY())); - } + newSettings.add(new WiredMatchFurniSetting(it.getId(), this.checkForWiredResetPermission && it.allowWiredResetState() ? it.getExtradata() : " ", it.getRotation(), it.getX(), it.getY(), it.getZ())); } int delay = settings.getDelay(); @@ -248,10 +286,9 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int this.state = setState; this.direction = setDirection; this.position = setPosition; + this.altitude = setAltitude; this.settings.clear(); - if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { - this.settings.addAll(newSettings); - } + this.settings.addAll(newSettings); this.setDelay(delay); return true; @@ -286,18 +323,25 @@ public class WiredEffectMatchFurni extends InteractionWiredEffect implements Int return this.position; } + @Override + public boolean shouldMatchAltitude() { + return this.altitude; + } + static class JsonData { boolean state; boolean direction; boolean position; + boolean altitude; List items; int delay; int furniSource; - public JsonData(boolean state, boolean direction, boolean position, List items, int delay, int furniSource) { + public JsonData(boolean state, boolean direction, boolean position, boolean altitude, List items, int delay, int furniSource) { this.state = state; this.direction = direction; this.position = position; + this.altitude = altitude; this.items = items; this.delay = delay; this.furniSource = furniSource; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveFurniAway.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveFurniAway.java index 3cd56464..ce14ea24 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveFurniAway.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveFurniAway.java @@ -10,11 +10,11 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; import com.eu.habbo.habbohotel.wired.core.WiredSimulation; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; -import com.eu.habbo.messages.outgoing.rooms.items.FloorItemOnRollerComposer; import gnu.trove.set.hash.THashSet; import java.sql.ResultSet; @@ -98,11 +98,10 @@ public class WiredEffectMoveFurniAway extends InteractionWiredEffect { RoomTile newLocation = room.getLayout().getTile((short) (item.getX() + x), (short) (item.getY() + y)); RoomTile oldLocation = room.getLayout().getTile(item.getX(), item.getY()); - double oldZ = item.getZ(); - if(newLocation != null && newLocation.state != RoomTileState.INVALID && newLocation != oldLocation && room.furnitureFitsAt(newLocation, item, item.getRotation(), true) == FurnitureMovementError.NONE) { - if(room.moveFurniTo(item, newLocation, item.getRotation(), null, false) == FurnitureMovementError.NONE) { - room.sendComposer(new FloorItemOnRollerComposer(item, null, oldLocation, oldZ, newLocation, item.getZ(), 0, room).compose()); + if (newLocation != null && newLocation.state != RoomTileState.INVALID && newLocation != oldLocation + && WiredMoveCarryHelper.getMovementError(room, this, item, newLocation, item.getRotation(), ctx) == FurnitureMovementError.NONE) { + if (WiredMoveCarryHelper.moveFurni(room, this, item, newLocation, item.getRotation(), null, false, ctx) == FurnitureMovementError.NONE) { } } } @@ -264,6 +263,10 @@ public class WiredEffectMoveFurniAway extends InteractionWiredEffect { throw new WiredSaveException("Too many furni selected"); } + if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + List newItems = new ArrayList<>(); if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveFurniTo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveFurniTo.java index d2087c9f..291fc053 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveFurniTo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveFurniTo.java @@ -5,6 +5,7 @@ import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.FurnitureMovementError; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnit; @@ -12,13 +13,12 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; import com.eu.habbo.habbohotel.wired.core.WiredSimulation; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; -import com.eu.habbo.messages.outgoing.rooms.items.FloorItemOnRollerComposer; import gnu.trove.set.hash.THashSet; - import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -59,6 +59,11 @@ public class WiredEffectMoveFurniTo extends InteractionWiredEffect { this.furniSource = settings.getIntParams()[2]; int count = settings.getFurniIds().length; + + if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { for (int i = 0; i < count; i++) { this.items.add(room.getHabboItem(settings.getFurniIds()[i])); @@ -112,11 +117,6 @@ public class WiredEffectMoveFurniTo extends InteractionWiredEffect { RoomTile objectTile = room.getLayout().getTile(targetItem.getX(), targetItem.getY()); if (objectTile != null) { - RoomTile sourceTile = room.getLayout().getTile(((HabboItem) object).getX(), ((HabboItem) object).getY()); - if (sourceTile == null) continue; - - THashSet refreshTiles = room.getLayout().getTilesAt(sourceTile, ((HabboItem) object).getBaseItem().getWidth(), ((HabboItem) object).getBaseItem().getLength(), ((HabboItem) object).getRotation()); - RoomTile tile = room.getLayout().getTileInFront(objectTile, this.direction, indexOffset); if (tile == null || !tile.getAllowStack()) { indexOffset = 0; @@ -127,13 +127,18 @@ public class WiredEffectMoveFurniTo extends InteractionWiredEffect { continue; } - room.sendComposer(new FloorItemOnRollerComposer((HabboItem) object, null, tile, tile.getStackHeight() - ((HabboItem) object).getZ(), room).compose()); - - RoomTile newSourceTile = room.getLayout().getTile(((HabboItem) object).getX(), ((HabboItem) object).getY()); - if (newSourceTile != null) { - refreshTiles.addAll(room.getLayout().getTilesAt(newSourceTile, ((HabboItem) object).getBaseItem().getWidth(), ((HabboItem) object).getBaseItem().getLength(), ((HabboItem) object).getRotation())); + HabboItem movingItem = (HabboItem) object; + RoomTile oldLocation = room.getLayout().getTile(movingItem.getX(), movingItem.getY()); + if (oldLocation == null) { + continue; } - room.updateTiles(refreshTiles); + + FurnitureMovementError movementError = WiredMoveCarryHelper.moveFurni(room, this, movingItem, tile, movingItem.getRotation(), null, false, ctx); + + if (movementError != FurnitureMovementError.NONE) { + continue; + } + this.indexOffset.put(targetItem.getId(), indexOffset); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveFurniTowards.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveFurniTowards.java index d01a9fc5..cde1e962 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveFurniTowards.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveFurniTowards.java @@ -10,11 +10,11 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; import com.eu.habbo.habbohotel.wired.core.WiredSimulation; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; -import com.eu.habbo.messages.outgoing.rooms.items.FloorItemOnRollerComposer; import com.eu.habbo.threading.runnables.WiredCollissionRunnable; import gnu.trove.map.hash.THashMap; import gnu.trove.set.hash.THashSet; @@ -53,7 +53,7 @@ public class WiredEffectMoveFurniTowards extends InteractionWiredEffect { this.lastDirections = new THashMap<>(); } - public List getAvailableDirections(HabboItem item, Room room) { + public List getAvailableDirections(HabboItem item, Room room, WiredContext ctx) { List availableDirections = new ArrayList<>(); RoomLayout layout = room.getLayout(); @@ -73,7 +73,7 @@ public class WiredEffectMoveFurniTowards extends InteractionWiredEffect { if (!layout.tileExists(tile.x, tile.y)) continue; - if (room.furnitureFitsAt(tile, item, item.getRotation()) == FurnitureMovementError.INVALID_MOVE) + if (WiredMoveCarryHelper.getMovementError(room, this, item, tile, item.getRotation(), ctx) != FurnitureMovementError.NONE) continue; HabboItem topItem = room.getTopItemAt(tile.x, tile.y); @@ -192,7 +192,7 @@ public class WiredEffectMoveFurniTowards extends InteractionWiredEffect { 3+ available - move in random direction, but never the opposite */ - List availableDirections = this.getAvailableDirections(item, room); + List availableDirections = this.getAvailableDirections(item, room, ctx); if (moveDirection != null && !availableDirections.contains(moveDirection)) moveDirection = null; @@ -228,13 +228,11 @@ public class WiredEffectMoveFurniTowards extends InteractionWiredEffect { RoomTile newTile = room.getLayout().getTileInFront(oldLocation, moveDirection.getValue()); - double oldZ = item.getZ(); - if(newTile != null) { lastDirections.put(item.getId(), moveDirection); - if(newTile.state != RoomTileState.INVALID && newTile != oldLocation && room.furnitureFitsAt(newTile, item, item.getRotation(), true) == FurnitureMovementError.NONE) { - if (room.moveFurniTo(item, newTile, item.getRotation(), null, false) == FurnitureMovementError.NONE) { - room.sendComposer(new FloorItemOnRollerComposer(item, null, oldLocation, oldZ, newTile, item.getZ(), 0, room).compose()); + if (newTile.state != RoomTileState.INVALID && newTile != oldLocation + && WiredMoveCarryHelper.getMovementError(room, this, item, newTile, item.getRotation(), ctx) == FurnitureMovementError.NONE) { + if (WiredMoveCarryHelper.moveFurni(room, this, item, newTile, item.getRotation(), null, false, ctx) == FurnitureMovementError.NONE) { } } } @@ -417,6 +415,10 @@ public class WiredEffectMoveFurniTowards extends InteractionWiredEffect { throw new WiredSaveException("Too many furni selected"); } + if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + List newItems = new ArrayList<>(); if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateFurni.java index 0b4cbb55..52bbe142 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateFurni.java @@ -11,11 +11,11 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; import com.eu.habbo.habbohotel.wired.core.WiredSimulation; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; -import com.eu.habbo.messages.outgoing.rooms.items.FloorItemOnRollerComposer; import gnu.trove.set.hash.THashSet; import java.sql.ResultSet; @@ -61,7 +61,6 @@ public class WiredEffectMoveRotateFurni extends InteractionWiredEffect implement int newRotation = this.rotation > 0 ? this.getNewRotation(item) : item.getRotation(); RoomTile newLocation = room.getLayout().getTile(item.getX(), item.getY()); RoomTile oldLocation = room.getLayout().getTile(item.getX(), item.getY()); - double oldZ = item.getZ(); if(this.direction > 0) { // Use pre-selected direction if available, otherwise pick random @@ -77,13 +76,14 @@ public class WiredEffectMoveRotateFurni extends InteractionWiredEffect implement boolean slideAnimation = item.getRotation() == newRotation; - FurnitureMovementError furniMoveTest = room.furnitureFitsAt(newLocation, item, newRotation, true); - if(newLocation != null && newLocation.state != RoomTileState.INVALID && (newLocation != oldLocation || newRotation != item.getRotation()) && (furniMoveTest == FurnitureMovementError.NONE || ((furniMoveTest == FurnitureMovementError.TILE_HAS_BOTS || furniMoveTest == FurnitureMovementError.TILE_HAS_HABBOS || furniMoveTest == FurnitureMovementError.TILE_HAS_PETS) && newLocation == oldLocation))) { - if(room.furnitureFitsAt(newLocation, item, newRotation, false) == FurnitureMovementError.NONE && room.moveFurniTo(item, newLocation, newRotation, null, !slideAnimation) == FurnitureMovementError.NONE) { + FurnitureMovementError furniMoveTest = WiredMoveCarryHelper.getMovementError(room, this, item, newLocation, newRotation, ctx); + if (newLocation != null && newLocation.state != RoomTileState.INVALID && (newLocation != oldLocation || newRotation != item.getRotation()) + && (furniMoveTest == FurnitureMovementError.NONE + || ((furniMoveTest == FurnitureMovementError.TILE_HAS_BOTS + || furniMoveTest == FurnitureMovementError.TILE_HAS_HABBOS + || furniMoveTest == FurnitureMovementError.TILE_HAS_PETS) && newLocation == oldLocation))) { + if (WiredMoveCarryHelper.moveFurni(room, this, item, newLocation, newRotation, null, !slideAnimation, ctx) == FurnitureMovementError.NONE) { this.itemCooldowns.add(item); - if(slideAnimation) { - room.sendComposer(new FloorItemOnRollerComposer(item, null, oldLocation, oldZ, newLocation, item.getZ(), 0, room).compose()); - } } } } @@ -261,6 +261,10 @@ public class WiredEffectMoveRotateFurni extends InteractionWiredEffect implement int count = settings.getFurniIds().length; if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5)) return false; + if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + this.items.clear(); if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { for (int i = 0; i < count; i++) { @@ -352,27 +356,32 @@ public class WiredEffectMoveRotateFurni extends InteractionWiredEffect implement * @return direction */ private RoomUserRotation getMovementDirection() { - RoomUserRotation movemementDirection = RoomUserRotation.NORTH; - if (this.direction == 1) { - movemementDirection = RoomUserRotation.values()[Emulator.getRandom().nextInt(RoomUserRotation.values().length / 2) * 2]; - } else if (this.direction == 2) { - if (Emulator.getRandom().nextInt(2) == 1) { - movemementDirection = RoomUserRotation.EAST; - } else { - movemementDirection = RoomUserRotation.WEST; - } - } else if (this.direction == 3) { - if (Emulator.getRandom().nextInt(2) != 1) { - movemementDirection = RoomUserRotation.SOUTH; - } - } else if (this.direction == 4) { - movemementDirection = RoomUserRotation.SOUTH; - } else if (this.direction == 5) { - movemementDirection = RoomUserRotation.EAST; - } else if (this.direction == 7) { - movemementDirection = RoomUserRotation.WEST; + switch (this.direction) { + case 1: + return RoomUserRotation.values()[Emulator.getRandom().nextInt(RoomUserRotation.values().length / 2) * 2]; + case 2: + return Emulator.getRandom().nextInt(2) == 1 ? RoomUserRotation.EAST : RoomUserRotation.WEST; + case 3: + return Emulator.getRandom().nextInt(2) == 1 ? RoomUserRotation.NORTH : RoomUserRotation.SOUTH; + case 4: + return RoomUserRotation.SOUTH; + case 5: + return RoomUserRotation.EAST; + case 6: + return RoomUserRotation.NORTH; + case 7: + return RoomUserRotation.WEST; + case 8: + return RoomUserRotation.NORTH_EAST; + case 9: + return RoomUserRotation.SOUTH_EAST; + case 10: + return RoomUserRotation.SOUTH_WEST; + case 11: + return RoomUserRotation.NORTH_WEST; + default: + return RoomUserRotation.NORTH; } - return movemementDirection; } @Override diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateUser.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateUser.java new file mode 100644 index 00000000..5df4fdc2 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateUser.java @@ -0,0 +1,291 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomTileState; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUserRotation; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredMovementPhysics; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import gnu.trove.procedure.TObjectProcedure; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredEffectMoveRotateUser extends InteractionWiredEffect { + private static final int ROTATION_CLOCKWISE = 8; + private static final int ROTATION_COUNTER_CLOCKWISE = 9; + + public static final WiredEffectType type = WiredEffectType.MOVE_ROTATE_USER; + + private int movementDirection = -1; + private int rotationDirection = -1; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredEffectMoveRotateUser(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectMoveRotateUser(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + WiredMovementPhysics movementPhysics = WiredMoveCarryHelper.getUserMovementPhysics(room, this, ctx); + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.userSource)) { + if (roomUnit == null || roomUnit.getRoom() != room) { + continue; + } + + boolean hasRotation = this.rotationDirection >= 0; + RoomUserRotation targetBodyRotation = hasRotation ? this.getTargetRotation(roomUnit) : roomUnit.getBodyRotation(); + RoomUserRotation targetHeadRotation = hasRotation ? targetBodyRotation : roomUnit.getHeadRotation(); + + if (roomUnit.isWalking()) { + if (hasRotation) { + WiredUserMovementHelper.updateUserDirection(room, roomUnit, targetBodyRotation, targetHeadRotation); + } + continue; + } + + RoomTile targetTile = (this.movementDirection >= 0) ? this.getTargetTile(room, roomUnit, this.movementDirection) : null; + boolean canMove = this.canMoveTo(room, roomUnit, targetTile, movementPhysics); + boolean noAnimation = WiredMoveCarryHelper.hasNoAnimationExtra(room, this); + int animationDuration = noAnimation ? 0 : WiredMoveCarryHelper.getAnimationDuration(room, this, WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION); + + if (canMove) { + double targetZ = targetTile.getStackHeight() + ((targetTile.state == RoomTileState.SIT) ? -0.5 : 0); + if (!WiredUserMovementHelper.moveUser(room, roomUnit, targetTile, targetZ, targetBodyRotation, targetHeadRotation, + animationDuration, noAnimation, movementPhysics)) { + if (hasRotation) { + WiredUserMovementHelper.updateUserDirection(room, roomUnit, targetBodyRotation, targetHeadRotation); + } + } + continue; + } + + if (hasRotation) { + WiredUserMovementHelper.updateUserDirection(room, roomUnit, targetBodyRotation, targetHeadRotation); + } + } + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.getDelay(), + this.movementDirection, + this.rotationDirection, + this.userSource + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + String wiredData = set.getString("wired_data"); + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.setDelay(data.delay); + this.movementDirection = this.normalizeDirection(data.movementDirection); + this.rotationDirection = this.normalizeRotation(data.rotationDirection); + this.userSource = data.userSource; + return; + } + + this.setDelay(0); + this.movementDirection = -1; + this.rotationDirection = -1; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + } + + @Override + public void onPickUp() { + this.setDelay(0); + this.movementDirection = -1; + this.rotationDirection = -1; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(3); + message.appendInt(this.movementDirection); + message.appendInt(this.rotationDirection); + message.appendInt(this.userSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + + if (this.requiresTriggeringUser()) { + List invalidTriggers = new ArrayList<>(); + room.getRoomSpecialTypes().getTriggers(this.getX(), this.getY()).forEach(new TObjectProcedure() { + @Override + public boolean execute(InteractionWiredTrigger object) { + if (!object.isTriggeredByRoomUnit()) { + invalidTriggers.add(object.getBaseItem().getSpriteId()); + } + return true; + } + }); + message.appendInt(invalidTriggers.size()); + for (Integer i : invalidTriggers) { + message.appendInt(i); + } + } else { + message.appendInt(0); + } + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + if (settings.getIntParams().length < 3) { + throw new WiredSaveException("Invalid data"); + } + + int delay = settings.getDelay(); + if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { + throw new WiredSaveException("Delay too long"); + } + + this.movementDirection = this.normalizeDirection(settings.getIntParams()[0]); + this.rotationDirection = this.normalizeRotation(settings.getIntParams()[1]); + this.userSource = settings.getIntParams()[2]; + this.setDelay(delay); + + return true; + } + + @Override + public boolean requiresTriggeringUser() { + return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; + } + + @Override + protected long requiredCooldown() { + return COOLDOWN_MOVEMENT; + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + private int normalizeDirection(int direction) { + return (direction >= 0 && direction <= 7) ? direction : -1; + } + + private int normalizeRotation(int rotation) { + return ((rotation >= 0 && rotation <= 7) || rotation == ROTATION_CLOCKWISE || rotation == ROTATION_COUNTER_CLOCKWISE) ? rotation : -1; + } + + private RoomUserRotation getTargetRotation(RoomUnit roomUnit) { + RoomUserRotation currentRotation = (roomUnit != null && roomUnit.getBodyRotation() != null) ? roomUnit.getBodyRotation() : RoomUserRotation.NORTH; + + if (this.rotationDirection == ROTATION_CLOCKWISE) { + return RoomUserRotation.clockwise(currentRotation); + } + + if (this.rotationDirection == ROTATION_COUNTER_CLOCKWISE) { + return RoomUserRotation.counterClockwise(currentRotation); + } + + return RoomUserRotation.fromValue(this.rotationDirection); + } + + private RoomTile getTargetTile(Room room, RoomUnit roomUnit, int direction) { + RoomTile currentTile = roomUnit.getCurrentLocation(); + + if (currentTile == null) { + return null; + } + + int deltaX = 0; + int deltaY = 0; + + switch (RoomUserRotation.fromValue(direction)) { + case NORTH: + deltaY = 1; + break; + case NORTH_EAST: + deltaX = 1; + deltaY = 1; + break; + case EAST: + deltaX = 1; + break; + case SOUTH_EAST: + deltaX = 1; + deltaY = -1; + break; + case SOUTH: + deltaY = -1; + break; + case SOUTH_WEST: + deltaX = -1; + deltaY = -1; + break; + case WEST: + deltaX = -1; + break; + case NORTH_WEST: + deltaX = -1; + deltaY = 1; + break; + } + + return room.getLayout().getTile((short) (currentTile.x + deltaX), (short) (currentTile.y + deltaY)); + } + + private boolean canMoveTo(Room room, RoomUnit roomUnit, RoomTile targetTile, WiredMovementPhysics movementPhysics) { + return WiredUserMovementHelper.canMoveTo(room, roomUnit, targetTile, movementPhysics); + } + + public static boolean handleWalkWhileActive(Room room, RoomUnit roomUnit, RoomTile targetTile) { + return false; + } + + static class JsonData { + int delay; + int movementDirection; + int rotationDirection; + int userSource; + + public JsonData(int delay, int movementDirection, int rotationDirection, int userSource) { + this.delay = delay; + this.movementDirection = movementDirection; + this.rotationDirection = rotationDirection; + this.userSource = userSource; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMuteHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMuteHabbo.java index 41acb60e..e7a66737 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMuteHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMuteHabbo.java @@ -14,6 +14,7 @@ import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTextPlaceholderUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserWhisperComposer; @@ -76,9 +77,11 @@ public class WiredEffectMuteHabbo extends InteractionWiredEffect { if (room.hasRights(habbo)) continue; - room.muteHabbo(habbo, 60); + room.muteHabbo(habbo, Math.max(1, this.length)); - habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(this.message.replace("%user%", habbo.getHabboInfo().getUsername()).replace("%online_count%", Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "").replace("%room_count%", Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size() + ""), habbo, habbo, RoomChatMessageBubbles.WIRED))); + String message = this.message.replace("%user%", habbo.getHabboInfo().getUsername()).replace("%online_count%", Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "").replace("%room_count%", Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size() + ""); + message = WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, message); + habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(message, habbo, habbo, RoomChatMessageBubbles.WIRED))); } } @@ -137,7 +140,7 @@ public class WiredEffectMuteHabbo extends InteractionWiredEffect { @Override public boolean requiresTriggeringUser() { - return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; + return this.userSource == WiredSourceUtil.SOURCE_TRIGGER || WiredTextPlaceholderUtil.requiresActor(this.getRoom(), this); } static class JsonData { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectNegativeSendSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectNegativeSendSignal.java new file mode 100644 index 00000000..14c54913 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectNegativeSendSignal.java @@ -0,0 +1,31 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredEffectNegativeSendSignal extends WiredEffectSendSignal { + public static final WiredEffectType type = WiredEffectType.NEG_SEND_SIGNAL; + + public WiredEffectNegativeSendSignal(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectNegativeSendSignal(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + protected boolean dispatchSignalEvent(WiredEvent event) { + return WiredManager.dispatchEffectTriggeredEvent(event); + } + + @Override + public WiredEffectType getType() { + return type; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectNegativeTriggerStacks.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectNegativeTriggerStacks.java new file mode 100644 index 00000000..ae46653b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectNegativeTriggerStacks.java @@ -0,0 +1,30 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredEffectNegativeTriggerStacks extends WiredEffectTriggerStacks { + public static final WiredEffectType type = WiredEffectType.NEG_CALL_STACKS; + + public WiredEffectNegativeTriggerStacks(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectNegativeTriggerStacks(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + super.execute(ctx); + } + + @Override + public WiredEffectType getType() { + return type; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRelativeMove.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRelativeMove.java new file mode 100644 index 00000000..9ba042d5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRelativeMove.java @@ -0,0 +1,284 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class WiredEffectRelativeMove extends InteractionWiredEffect { + private static final int HORIZONTAL_NEGATIVE = 0; + private static final int HORIZONTAL_POSITIVE = 1; + private static final int VERTICAL_NEGATIVE = 0; + private static final int VERTICAL_POSITIVE = 1; + private static final int MAX_DISTANCE = 20; + + public static final WiredEffectType type = WiredEffectType.RELATIVE_MOVE; + + private final List items = new ArrayList<>(); + private int horizontalDirection = HORIZONTAL_POSITIVE; + private int horizontalDistance = 0; + private int verticalDirection = VERTICAL_POSITIVE; + private int verticalDistance = 0; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredEffectRelativeMove(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectRelativeMove(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null || room.getLayout() == null) { + return; + } + + List effectiveItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + this.items.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + } + + int deltaX = this.getHorizontalOffset(); + int deltaY = this.getVerticalOffset(); + + if (deltaX == 0 && deltaY == 0) { + return; + } + + for (HabboItem item : effectiveItems) { + if (item == null || item.getRoomId() != this.getRoomId()) { + continue; + } + + short targetX = (short) (item.getX() + deltaX); + short targetY = (short) (item.getY() + deltaY); + + RoomTile targetTile = room.getLayout().getTile(targetX, targetY); + if (targetTile == null) { + continue; + } + + WiredMoveCarryHelper.moveFurni(room, this, item, targetTile, item.getRotation(), null, true, ctx); + } + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.getDelay(), + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), + this.horizontalDirection, + this.horizontalDistance, + this.verticalDirection, + this.verticalDistance, + this.furniSource + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.items.clear(); + String wiredData = set.getString("wired_data"); + + if (wiredData != null && wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.setDelay(data.delay); + this.horizontalDirection = this.normalizeBinary(data.horizontalDirection, HORIZONTAL_POSITIVE); + this.horizontalDistance = this.normalizeDistance(data.horizontalDistance); + this.verticalDirection = this.normalizeBinary(data.verticalDirection, VERTICAL_POSITIVE); + this.verticalDistance = this.normalizeDistance(data.verticalDistance); + this.furniSource = data.furniSource; + + if (data.itemIds != null) { + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + + if (item != null) { + this.items.add(item); + } + } + } + + return; + } + + this.horizontalDirection = HORIZONTAL_POSITIVE; + this.horizontalDistance = 0; + this.verticalDirection = VERTICAL_POSITIVE; + this.verticalDistance = 0; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.setDelay(0); + } + + @Override + public void onPickUp() { + this.items.clear(); + this.horizontalDirection = HORIZONTAL_POSITIVE; + this.horizontalDistance = 0; + this.verticalDirection = VERTICAL_POSITIVE; + this.verticalDistance = 0; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.setDelay(0); + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + List itemsSnapshot = new ArrayList<>(this.items); + itemsSnapshot.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + + this.items.clear(); + this.items.addAll(itemsSnapshot); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(itemsSnapshot.size()); + for (HabboItem item : itemsSnapshot) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(5); + message.appendInt(this.horizontalDirection); + message.appendInt(this.horizontalDistance); + message.appendInt(this.verticalDirection); + message.appendInt(this.verticalDistance); + message.appendInt(this.furniSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + int[] params = settings.getIntParams(); + + if (params.length < 5) { + throw new WiredSaveException("Invalid data"); + } + + this.horizontalDirection = this.normalizeBinary(params[0], HORIZONTAL_POSITIVE); + this.horizontalDistance = this.normalizeDistance(params[1]); + this.verticalDirection = this.normalizeBinary(params[2], VERTICAL_POSITIVE); + this.verticalDistance = this.normalizeDistance(params[3]); + this.furniSource = params[4]; + + int delay = settings.getDelay(); + if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { + throw new WiredSaveException("Delay too long"); + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + + List newItems = new ArrayList<>(); + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item == null) { + throw new WiredSaveException(String.format("Item %s not found", itemId)); + } + + newItems.add(item); + } + + this.items.clear(); + this.items.addAll(newItems); + this.setDelay(delay); + + return true; + } + + private int getHorizontalOffset() { + if (this.horizontalDistance <= 0) { + return 0; + } + + return (this.horizontalDirection == HORIZONTAL_NEGATIVE) ? -this.horizontalDistance : this.horizontalDistance; + } + + private int getVerticalOffset() { + if (this.verticalDistance <= 0) { + return 0; + } + + return (this.verticalDirection == VERTICAL_NEGATIVE) ? -this.verticalDistance : this.verticalDistance; + } + + private int normalizeBinary(int value, int fallback) { + if (value == 0 || value == 1) { + return value; + } + + return fallback; + } + + private int normalizeDistance(int value) { + return Math.max(0, Math.min(MAX_DISTANCE, value)); + } + + static class JsonData { + int delay; + List itemIds; + int horizontalDirection; + int horizontalDistance; + int verticalDirection; + int verticalDistance; + int furniSource; + + public JsonData(int delay, List itemIds, int horizontalDirection, int horizontalDistance, int verticalDirection, int verticalDistance, int furniSource) { + this.delay = delay; + this.itemIds = itemIds; + this.horizontalDirection = horizontalDirection; + this.horizontalDistance = horizontalDistance; + this.verticalDirection = verticalDirection; + this.verticalDistance = verticalDistance; + this.furniSource = furniSource; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRemoveVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRemoveVariable.java new file mode 100644 index 00000000..d2812e61 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRemoveVariable.java @@ -0,0 +1,370 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import gnu.trove.set.hash.THashSet; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredEffectRemoveVariable extends InteractionWiredEffect { + public static final WiredEffectType type = WiredEffectType.REMOVE_VAR; + + public static final int TARGET_USER = 0; + public static final int TARGET_FURNI = 1; + public static final int TARGET_CONTEXT = 2; + + private int variableItemId = 0; + private int targetType = TARGET_USER; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private final THashSet selectedFurni; + + public WiredEffectRemoveVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + this.selectedFurni = new THashSet<>(); + } + + public WiredEffectRemoveVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + this.selectedFurni = new THashSet<>(); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + + if (room == null) { + return; + } + + switch (this.targetType) { + case TARGET_USER: + this.executeUserVariables(ctx, room); + return; + case TARGET_FURNI: + this.executeFurniVariables(ctx, room); + return; + case TARGET_CONTEXT: + this.executeContextVariables(ctx, room); + return; + default: + return; + } + } + + private void executeUserVariables(WiredContext ctx, Room room) { + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.variableItemId); + + if (definition == null || definition.isReadOnly()) { + return; + } + + List users = WiredSourceUtil.resolveUsers(ctx, this.userSource); + + for (RoomUnit roomUnit : users) { + if (roomUnit == null) { + continue; + } + + Habbo habbo = room.getHabbo(roomUnit); + + if (habbo == null) { + continue; + } + + room.getUserVariableManager().removeVariable(habbo.getHabboInfo().getId(), this.variableItemId); + } + } + + private void executeFurniVariables(WiredContext ctx, Room room) { + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.variableItemId); + + if (definition == null || definition.isReadOnly()) { + return; + } + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + this.validateItems(this.selectedFurni); + } + + List furni = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.selectedFurni); + + for (HabboItem item : furni) { + if (item == null) { + continue; + } + + room.getFurniVariableManager().removeVariable(item.getId(), this.variableItemId); + } + } + + private void executeContextVariables(WiredContext ctx, Room room) { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, this.variableItemId); + + if (definition == null || definition.isReadOnly()) { + return; + } + + WiredContextVariableSupport.removeVariable(ctx, room, this.variableItemId); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + List selectedItems = new ArrayList<>(); + + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + for (HabboItem item : this.selectedFurni) { + if (item != null && room != null && room.getHabboItem(item.getId()) != null) { + selectedItems.add(item); + } + } + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(selectedItems.size()); + + for (HabboItem item : selectedItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(String.valueOf(this.variableItemId)); + message.appendInt(3); + message.appendInt(this.targetType); + message.appendInt(this.userSource); + message.appendInt(this.furniSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + int[] intParams = settings.getIntParams(); + int nextTargetType = normalizeTargetType((intParams.length > 0) ? intParams[0] : TARGET_USER); + int nextUserSource = normalizeUserSource((intParams.length > 1) ? intParams[1] : WiredSourceUtil.SOURCE_TRIGGER); + int nextFurniSource = normalizeFurniSource((intParams.length > 2) ? intParams[2] : WiredSourceUtil.SOURCE_TRIGGER); + int nextVariableItemId = parseVariableItemId(settings.getStringParam()); + + if (nextVariableItemId <= 0) { + throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + } + + switch (nextTargetType) { + case TARGET_USER: + WiredVariableDefinitionInfo userDefinition = room.getUserVariableManager().getDefinitionInfo(nextVariableItemId); + if (userDefinition == null || userDefinition.isReadOnly()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + break; + case TARGET_FURNI: + WiredVariableDefinitionInfo furniDefinition = room.getFurniVariableManager().getDefinitionInfo(nextVariableItemId); + if (furniDefinition == null || furniDefinition.isReadOnly()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + break; + case TARGET_CONTEXT: + WiredVariableDefinitionInfo contextDefinition = WiredContextVariableSupport.getDefinitionInfo(room, nextVariableItemId); + if (contextDefinition == null || contextDefinition.isReadOnly()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + break; + default: + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + this.selectedFurni.clear(); + + if (nextTargetType == TARGET_FURNI && nextFurniSource == WiredSourceUtil.SOURCE_SELECTED) { + int[] furniIds = settings.getFurniIds(); + int itemsCount = (furniIds != null) ? furniIds.length : 0; + + if (itemsCount > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + + for (int i = 0; i < itemsCount; i++) { + int itemId = furniIds[i]; + HabboItem item = room.getHabboItem(itemId); + + if (item == null) { + throw new WiredSaveException(String.format("Item %s not found", itemId)); + } + + this.selectedFurni.add(item); + } + } + + this.variableItemId = nextVariableItemId; + this.targetType = nextTargetType; + this.userSource = nextUserSource; + this.furniSource = nextFurniSource; + this.setDelay(settings.getDelay()); + + return true; + } + + @Override + public String getWiredData() { + List selectedItemIds = new ArrayList<>(); + + for (HabboItem item : this.selectedFurni) { + if (item != null) { + selectedItemIds.add(item.getId()); + } + } + + return WiredManager.getGson().toJson(new JsonData(this.variableItemId, this.targetType, this.userSource, this.furniSource, this.getDelay(), selectedItemIds)); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.variableItemId = Math.max(0, data.variableItemId); + this.targetType = normalizeTargetType(data.targetType); + this.userSource = normalizeUserSource(data.userSource); + this.furniSource = normalizeFurniSource(data.furniSource); + this.setDelay(Math.max(0, data.delay)); + + if (room != null && data.selectedFurniIds != null) { + for (Integer itemId : data.selectedFurniIds) { + if (itemId == null || itemId <= 0) { + continue; + } + + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + this.selectedFurni.add(item); + } + } + } + } + + return; + } + + try { + this.variableItemId = Math.max(0, Integer.parseInt(wiredData.trim())); + this.targetType = TARGET_USER; + } catch (NumberFormatException ignored) { + } + } + + @Override + public void onPickUp() { + this.variableItemId = 0; + this.targetType = TARGET_USER; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.selectedFurni.clear(); + this.setDelay(0); + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public WiredEffectType getType() { + return type; + } + + private static int normalizeTargetType(int value) { + switch (value) { + case TARGET_FURNI: + case TARGET_CONTEXT: + return value; + default: + return TARGET_USER; + } + } + + private static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private static int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + private static int parseVariableItemId(String value) { + if (value == null || value.trim().isEmpty()) { + return 0; + } + + try { + return Math.max(0, Integer.parseInt(value.trim())); + } catch (NumberFormatException ignored) { + return 0; + } + } + + static class JsonData { + int variableItemId; + int targetType; + int userSource; + int furniSource; + int delay; + List selectedFurniIds; + + JsonData(int variableItemId, int targetType, int userSource, int furniSource, int delay, List selectedFurniIds) { + this.variableItemId = variableItemId; + this.targetType = targetType; + this.userSource = userSource; + this.furniSource = furniSource; + this.delay = delay; + this.selectedFurniIds = selectedFurniIds; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java index 66a4429d..f9121ce5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java @@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -36,14 +37,16 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { private static final int ANTENNA_PICKED = 0; private static final int ANTENNA_TRIGGER = 1; - - private static final int FORWARD_NONE = 0; - private static final int FORWARD_TRIGGER = 1; + private static final String ANTENNA_INTERACTION = "antenna"; + private static final String FORWARD_ITEM_SPLIT_REGEX = "[;,\\t]"; + private static final long ANTENNA_PULSE_MS = 300L; + private static final ConcurrentHashMap ANTENNA_PULSE_TOKENS = new ConcurrentHashMap<>(); private THashSet items; + private THashSet forwardItems; private int antennaSource = ANTENNA_PICKED; - private int furniForward = FORWARD_NONE; - private int userForward = FORWARD_NONE; + private int furniForward = WiredSourceUtil.SOURCE_TRIGGER; + private int userForward = WiredSourceUtil.SOURCE_TRIGGER; private boolean signalPerFurni = false; private boolean signalPerUser = false; private int channel = 0; @@ -51,11 +54,13 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { public WiredEffectSendSignal(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); this.items = new THashSet<>(); + this.forwardItems = new THashSet<>(); } public WiredEffectSendSignal(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { super(id, userId, item, extradata, limitedStack, limitedSells); this.items = new THashSet<>(); + this.forwardItems = new THashSet<>(); } @Override @@ -77,82 +82,128 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { .map(Collections::singleton) .orElse(Collections.emptySet()); } else { - antennas = ctx.targets().isItemsModifiedBySelector() - ? new ArrayList<>(ctx.targets().items()) - : new ArrayList<>(this.items); + Collection baseAntennas = new ArrayList<>(this.items); + + if (baseAntennas.isEmpty() && antennaSource > ANTENNA_TRIGGER) { + HabboItem antenna = room.getHabboItem(antennaSource); + antennas = (antenna != null) ? Collections.singleton(antenna) : Collections.emptySet(); + } else { + antennas = baseAntennas; + } } - if (antennas.isEmpty()) { + List resolvedAntennas = antennas.stream() + .filter(Objects::nonNull) + .filter(this::isAntennaItem) + .collect(Collectors.toList()); + + if (resolvedAntennas.isEmpty()) { LOGGER.debug("[SendSignal] No antennas resolved, aborting. antennaSource={}, selectorModified={}", antennaSource, ctx.targets().isItemsModifiedBySelector()); return; } - LOGGER.debug("[SendSignal] Resolved {} antenna(s), firing signals", antennas.size()); + LOGGER.debug("[SendSignal] Resolved {} antenna(s), firing signals", resolvedAntennas.size()); - RoomUnit forwardedUser = null; - if (userForward == FORWARD_TRIGGER) { - forwardedUser = ctx.actor().orElse(null); - } + RoomUnit triggeringUser = ctx.event().getOriginActor().orElseGet(() -> ctx.actor().orElse(null)); + List forwardedUsers = WiredSourceUtil.resolveUsersRaw(ctx, this.userForward); + List forwardedFurni = WiredSourceUtil.resolveItemsRaw(ctx, this.furniForward, this.forwardItems); - HabboItem forwardedFurni = null; - if (furniForward == FORWARD_TRIGGER) { - forwardedFurni = ctx.sourceItem().orElse(null); - } + List usersToSend; + if (signalPerUser) { + LinkedHashMap mergedUsers = new LinkedHashMap<>(); - Set visitedTiles = new HashSet<>(); - List antennaTiles = new ArrayList<>(); - for (HabboItem antenna : antennas) { - if (antenna == null) continue; - String key = antenna.getX() + "," + antenna.getY(); - if (visitedTiles.add(key)) { - RoomTile tile = room.getLayout().getTile(antenna.getX(), antenna.getY()); - if (tile != null) { - antennaTiles.add(tile); - } + if (triggeringUser != null) { + mergedUsers.put(triggeringUser.getId(), triggeringUser); } + + for (RoomUnit forwardedUser : forwardedUsers) { + if (forwardedUser == null) { + continue; + } + + mergedUsers.put(forwardedUser.getId(), forwardedUser); + } + + usersToSend = mergedUsers.isEmpty() + ? Collections.singletonList(null) + : new ArrayList<>(mergedUsers.values()); + } else { + usersToSend = Collections.singletonList(triggeringUser); } + Collection furniToSend = !forwardedFurni.isEmpty() + ? forwardedFurni + : Collections.singletonList(null); + int nextDepth = currentDepth + 1; + int signalUserCount = signalPerUser + ? (int) usersToSend.stream().filter(Objects::nonNull).count() + : (!forwardedUsers.isEmpty() ? forwardedUsers.size() : (triggeringUser != null ? 1 : 0)); - if (signalPerFurni || signalPerUser) { - if (signalPerFurni) { - for (RoomTile tile : antennaTiles) { - fireSignalAtTile(room, tile, forwardedUser, forwardedFurni, nextDepth); + for (RoomUnit user : usersToSend) { + for (HabboItem sourceItem : furniToSend) { + for (HabboItem antenna : resolvedAntennas) { + fireSignalAtAntenna(ctx, room, antenna, user, triggeringUser, sourceItem, signalUserCount, nextDepth); } } - if (signalPerUser && ctx.targets().hasUsers()) { - for (RoomUnit user : ctx.targets().users()) { - for (RoomTile tile : antennaTiles) { - fireSignalAtTile(room, tile, user, forwardedFurni, nextDepth); - } - } - } else if (!signalPerFurni) { - for (RoomTile tile : antennaTiles) { - fireSignalAtTile(room, tile, forwardedUser, forwardedFurni, nextDepth); - } - } - } else { - for (RoomTile tile : antennaTiles) { - fireSignalAtTile(room, tile, forwardedUser, forwardedFurni, nextDepth); - } } } - private void fireSignalAtTile(Room room, RoomTile tile, RoomUnit actor, HabboItem sourceItem, int depth) { - LOGGER.debug("[SendSignal] fireSignalAtTile: tile={},{} depth={} channel={} actor={} sourceItem={}", tile.x, tile.y, depth, channel, actor != null ? actor.getId() : "null", sourceItem != null ? sourceItem.getId() : "null"); + private void fireSignalAtAntenna(WiredContext ctx, Room room, HabboItem antenna, RoomUnit actor, RoomUnit originActor, HabboItem sourceItem, int signalUserCount, int depth) { + if (antenna == null) return; + RoomTile tile = room.getLayout().getTile(antenna.getX(), antenna.getY()); + if (tile == null) return; + + pulseAntenna(room, antenna); + + int signalChannel = antenna.getId(); + + LOGGER.debug("[SendSignal] fireSignalAtAntenna: antennaId={} tile={},{} depth={} channel={} actor={} sourceItem={}", + signalChannel, tile.x, tile.y, depth, signalChannel, actor != null ? actor.getId() : "null", sourceItem != null ? sourceItem.getId() : "null"); WiredEvent.Builder builder = WiredEvent.builder(WiredEvent.Type.SIGNAL_RECEIVED, room) .tile(tile) .callStackDepth(depth) - .signalChannel(this.channel) + .signalChannel(signalChannel) + .signalUserCount(signalUserCount) + .signalFurniCount(sourceItem != null ? 1 : 0) + .contextVariableScope(ctx.contextVariables()) .triggeredByEffect(true); if (actor != null) builder.actor(actor); + if (originActor != null) builder.originActor(originActor); if (sourceItem != null) builder.sourceItem(sourceItem); - boolean result = WiredManager.handleEvent(builder.build()); + boolean result = dispatchSignalEvent(builder.build()); LOGGER.debug("[SendSignal] handleEvent returned: {}", result); } + private void pulseAntenna(Room room, HabboItem antenna) { + if (room == null || antenna == null || antenna.getBaseItem() == null) return; + if (antenna.getBaseItem().getStateCount() <= 1) return; + + final long token = System.currentTimeMillis(); + ANTENNA_PULSE_TOKENS.put(antenna.getId(), token); + + if ("1".equals(antenna.getExtradata())) { + antenna.setExtradata("0"); + room.updateItemState(antenna); + } + + antenna.setExtradata("1"); + room.updateItemState(antenna); + + Emulator.getThreading().run(() -> { + if (!room.isLoaded()) return; + + Long currentToken = ANTENNA_PULSE_TOKENS.get(antenna.getId()); + if (currentToken == null || currentToken.longValue() != token) return; + + antenna.setExtradata("0"); + room.updateItemState(antenna); + ANTENNA_PULSE_TOKENS.remove(antenna.getId(), token); + }, ANTENNA_PULSE_MS); + } + @Override public void serializeWiredData(ServerMessage message, Room room) { List itemsSnapshot = new ArrayList<>(this.items); @@ -161,6 +212,16 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { item.getRoomId() != this.getRoomId() || room.getHabboItem(item.getId()) == null); this.items.retainAll(itemsSnapshot); + List forwardSnapshot = new ArrayList<>(this.forwardItems); + forwardSnapshot.removeIf(item -> + item.getRoomId() != this.getRoomId() || room.getHabboItem(item.getId()) == null); + this.forwardItems.retainAll(forwardSnapshot); + + String forwardString = forwardSnapshot.stream() + .filter(Objects::nonNull) + .map(item -> Integer.toString(item.getId())) + .collect(Collectors.joining(";")); + message.appendBoolean(false); message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); message.appendInt(itemsSnapshot.size()); @@ -169,7 +230,7 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { } message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); - message.appendString(""); + message.appendString(forwardString); message.appendInt(6); message.appendInt(antennaSource); @@ -219,6 +280,12 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { newItems.add(it); } + for (HabboItem receiver : newItems) { + if (!isAntennaItem(receiver)) { + throw new WiredSaveException("Only antenna furni can be selected"); + } + } + if (room != null && room.getRoomSpecialTypes() != null) { for (HabboItem receiver : newItems) { int count = room.getRoomSpecialTypes().countSendersTargetingReceiver(receiver.getId(), this); @@ -234,18 +301,36 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { } int[] params = settings.getIntParams(); - this.antennaSource = params.length > 0 ? params[0] : ANTENNA_PICKED; - this.furniForward = params.length > 1 ? params[1] : FORWARD_NONE; - this.userForward = params.length > 2 ? params[2] : FORWARD_NONE; + int requestedAntennaSource = params.length > 0 ? params[0] : ANTENNA_PICKED; + this.furniForward = normalizeSource(params.length > 1 ? params[1] : WiredSourceUtil.SOURCE_TRIGGER); + this.userForward = normalizeSource(params.length > 2 ? params[2] : WiredSourceUtil.SOURCE_TRIGGER); this.signalPerFurni = params.length > 3 && params[3] == 1; this.signalPerUser = params.length > 4 && params[4] == 1; this.channel = params.length > 5 ? params[5] : 0; + this.antennaSource = requestedAntennaSource; + if (!newItems.isEmpty()) { + this.antennaSource = newItems.get(0).getId(); + } + + List newForwardItems = new ArrayList<>(); + if (this.furniForward == WiredSourceUtil.SOURCE_SELECTED && room != null) { + newForwardItems = parseForwardItems(settings.getStringParam(), room); + } + if (newForwardItems.size() > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + this.items.clear(); this.items.addAll(newItems); + + this.forwardItems.clear(); + if (this.furniForward == WiredSourceUtil.SOURCE_SELECTED) { + this.forwardItems.addAll(newForwardItems); + } this.setDelay(delay); - LOGGER.debug("[SendSignal] saveData: antennaSource={}, furniForward={}, userForward={}, signalPerFurni={}, signalPerUser={}, channel={}, items={}", - antennaSource, furniForward, userForward, signalPerFurni, signalPerUser, channel, items.size()); + LOGGER.debug("[SendSignal] saveData: antennaSource={}, furniForward={}, userForward={}, signalPerFurni={}, signalPerUser={}, channel={}, items={}, forwardItems={}", + antennaSource, furniForward, userForward, signalPerFurni, signalPerUser, channel, items.size(), forwardItems.size()); return true; } @@ -259,9 +344,11 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { @Override public String getWiredData() { List itemsSnapshot = new ArrayList<>(this.items); + List forwardSnapshot = new ArrayList<>(this.forwardItems); return WiredManager.getGson().toJson(new JsonData( this.getDelay(), itemsSnapshot.stream().map(HabboItem::getId).collect(Collectors.toList()), + forwardSnapshot.stream().map(HabboItem::getId).collect(Collectors.toList()), antennaSource, furniForward, userForward, signalPerFurni, signalPerUser, channel )); } @@ -269,14 +356,15 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { this.items = new THashSet<>(); + this.forwardItems = new THashSet<>(); String wiredData = set.getString("wired_data"); if (wiredData != null && wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.setDelay(data.delay); this.antennaSource = data.antennaSource; - this.furniForward = data.furniForward; - this.userForward = data.userForward; + this.furniForward = normalizeSource(data.furniForward); + this.userForward = normalizeSource(data.userForward); this.signalPerFurni = data.signalPerFurni; this.signalPerUser = data.signalPerUser; this.channel = data.channel; @@ -286,21 +374,84 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { if (item != null) this.items.add(item); } } + if (data.forwardItemIds != null) { + for (Integer id : data.forwardItemIds) { + HabboItem item = room.getHabboItem(id); + if (item != null) this.forwardItems.add(item); + } + } + + if (this.antennaSource <= ANTENNA_TRIGGER && !this.items.isEmpty()) { + HabboItem first = this.items.iterator().next(); + if (first != null) this.antennaSource = first.getId(); + } } } @Override public void onPickUp() { this.items.clear(); + this.forwardItems.clear(); this.antennaSource = ANTENNA_PICKED; - this.furniForward = FORWARD_NONE; - this.userForward = FORWARD_NONE; + this.furniForward = WiredSourceUtil.SOURCE_TRIGGER; + this.userForward = WiredSourceUtil.SOURCE_TRIGGER; this.signalPerFurni = false; this.signalPerUser = false; this.channel = 0; this.setDelay(0); } + private int normalizeSource(int source) { + if (source == 1) return WiredSourceUtil.SOURCE_TRIGGER; + if (source == WiredSourceUtil.SOURCE_TRIGGER + || source == WiredSourceUtil.SOURCE_SELECTED + || source == WiredSourceUtil.SOURCE_SELECTOR + || source == WiredSourceUtil.SOURCE_SIGNAL) { + return source; + } + return WiredSourceUtil.SOURCE_TRIGGER; + } + + private List parseForwardItems(String data, Room room) throws WiredSaveException { + List results = new ArrayList<>(); + if (data == null || data.trim().isEmpty() || room == null) return results; + + Set seen = new HashSet<>(); + String[] parts = data.split(FORWARD_ITEM_SPLIT_REGEX); + + for (String part : parts) { + if (part == null) continue; + + String trimmed = part.trim(); + if (trimmed.isEmpty()) continue; + + int itemId; + try { + itemId = Integer.parseInt(trimmed); + } catch (NumberFormatException e) { + continue; + } + + if (itemId <= 0 || !seen.add(itemId)) continue; + + HabboItem item = room.getHabboItem(itemId); + if (item == null) throw new WiredSaveException(String.format("Item %s not found", itemId)); + + results.add(item); + } + + return results; + } + + private boolean isAntennaItem(HabboItem item) { + if (item == null || item.getBaseItem() == null || item.getBaseItem().getInteractionType() == null) return false; + String interaction = item.getBaseItem().getInteractionType().getName(); + if (interaction == null) return false; + + String normalized = interaction.toLowerCase(); + return normalized.equals(ANTENNA_INTERACTION); + } + @Override public WiredEffectType getType() { return type; @@ -320,14 +471,56 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { return false; } + public boolean unlinkAntenna(int antennaItemId) { + if (antennaItemId <= 0) { + return false; + } + + boolean changed = false; + + Iterator iterator = this.items.iterator(); + while (iterator.hasNext()) { + HabboItem item = iterator.next(); + + if (item == null || item.getId() != antennaItemId) { + continue; + } + + iterator.remove(); + changed = true; + } + + if (this.antennaSource == antennaItemId) { + if (!this.items.isEmpty()) { + HabboItem firstItem = this.items.iterator().next(); + this.antennaSource = (firstItem != null) ? firstItem.getId() : ANTENNA_PICKED; + } else { + this.antennaSource = ANTENNA_PICKED; + } + + changed = true; + } + + if (changed) { + this.needsUpdate(true); + } + + return changed; + } + @Override protected long requiredCooldown() { return COOLDOWN_TRIGGER_STACKS; } + protected boolean dispatchSignalEvent(WiredEvent event) { + return WiredManager.dispatchEffectTriggeredEvent(event); + } + static class JsonData { int delay; List itemIds; + List forwardItemIds; int antennaSource; int furniForward; int userForward; @@ -335,10 +528,11 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { boolean signalPerUser; int channel; - public JsonData(int delay, List itemIds, int antennaSource, int furniForward, + public JsonData(int delay, List itemIds, List forwardItemIds, int antennaSource, int furniForward, int userForward, boolean signalPerFurni, boolean signalPerUser, int channel) { this.delay = delay; this.itemIds = itemIds; + this.forwardItemIds = forwardItemIds; this.antennaSource = antennaSource; this.furniForward = furniForward; this.userForward = userForward; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSetAltitude.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSetAltitude.java new file mode 100644 index 00000000..9233daed --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSetAltitude.java @@ -0,0 +1,289 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class WiredEffectSetAltitude extends InteractionWiredEffect { + private static final Pattern ALTITUDE_PATTERN = Pattern.compile("^\\d+(\\.\\d{1,2})?$"); + + private static final int OPERATOR_INCREASE = 0; + private static final int OPERATOR_DECREASE = 1; + private static final int OPERATOR_SET = 2; + + public static final WiredEffectType type = WiredEffectType.SET_ALTITUDE; + + private final List items = new ArrayList<>(); + private int operator = OPERATOR_SET; + private double altitude = 0.0D; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredEffectSetAltitude(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectSetAltitude(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) { + return; + } + + List effectiveItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + this.items.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + } + + for (HabboItem item : effectiveItems) { + if (item == null || item.getRoomId() != this.getRoomId()) { + continue; + } + + RoomTile tile = room.getLayout().getTile(item.getX(), item.getY()); + if (tile == null) { + continue; + } + + double nextAltitude = this.computeAltitude(item.getZ()); + WiredMoveCarryHelper.moveFurni(room, this, item, tile, item.getRotation(), nextAltitude, null, true, ctx); + } + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.getDelay(), + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), + this.operator, + this.formatAltitude(this.altitude), + this.furniSource + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.items.clear(); + String wiredData = set.getString("wired_data"); + + if (wiredData != null && wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.setDelay(data.delay); + this.operator = this.normalizeOperator(data.operator); + this.altitude = this.parseAltitudeOrDefault(data.altitude); + this.furniSource = data.furniSource; + + if (data.itemIds != null) { + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + + if (item != null) { + this.items.add(item); + } + } + } + + return; + } + + this.operator = OPERATOR_SET; + this.altitude = 0.0D; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.setDelay(0); + } + + @Override + public void onPickUp() { + this.items.clear(); + this.operator = OPERATOR_SET; + this.altitude = 0.0D; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.setDelay(0); + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + List itemsSnapshot = new ArrayList<>(this.items); + itemsSnapshot.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + + this.items.clear(); + this.items.addAll(itemsSnapshot); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(itemsSnapshot.size()); + for (HabboItem item : itemsSnapshot) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.formatAltitude(this.altitude)); + message.appendInt(2); + message.appendInt(this.operator); + message.appendInt(this.furniSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + int[] params = settings.getIntParams(); + this.operator = (params.length > 0) ? this.normalizeOperator(params[0]) : OPERATOR_SET; + this.furniSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + + int delay = settings.getDelay(); + if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { + throw new WiredSaveException("Delay too long"); + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + + List newItems = new ArrayList<>(); + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item == null) { + throw new WiredSaveException(String.format("Item %s not found", itemId)); + } + + newItems.add(item); + } + + this.altitude = this.parseAltitude(settings.getStringParam()); + this.items.clear(); + this.items.addAll(newItems); + this.setDelay(delay); + + return true; + } + + private int normalizeOperator(int value) { + if (value < OPERATOR_INCREASE || value > OPERATOR_SET) { + return OPERATOR_SET; + } + + return value; + } + + private double computeAltitude(double currentAltitude) { + double nextAltitude; + + switch (this.operator) { + case OPERATOR_INCREASE: + nextAltitude = currentAltitude + this.altitude; + break; + case OPERATOR_DECREASE: + nextAltitude = currentAltitude - this.altitude; + break; + case OPERATOR_SET: + default: + nextAltitude = this.altitude; + break; + } + + return this.normalizeAltitude(nextAltitude); + } + + private double parseAltitude(String value) throws WiredSaveException { + String normalized = (value != null) ? value.trim() : ""; + + if (normalized.isEmpty()) { + return 0.0D; + } + + if (!ALTITUDE_PATTERN.matcher(normalized).matches()) { + throw new WiredSaveException("Invalid altitude value"); + } + + try { + return this.normalizeAltitude(new BigDecimal(normalized).doubleValue()); + } catch (NumberFormatException exception) { + throw new WiredSaveException("Invalid altitude value"); + } + } + + private double parseAltitudeOrDefault(String value) { + try { + return this.parseAltitude(value); + } catch (WiredSaveException exception) { + return 0.0D; + } + } + + private double normalizeAltitude(double value) { + double clampedValue = Math.max(0.0D, Math.min(Room.MAXIMUM_FURNI_HEIGHT, value)); + return BigDecimal.valueOf(clampedValue).setScale(2, RoundingMode.HALF_UP).doubleValue(); + } + + private String formatAltitude(double value) { + BigDecimal decimal = BigDecimal.valueOf(this.normalizeAltitude(value)).stripTrailingZeros(); + return (decimal.scale() < 0 ? decimal.setScale(0, RoundingMode.DOWN) : decimal).toPlainString(); + } + + static class JsonData { + int delay; + List itemIds; + int operator; + String altitude; + int furniSource; + + public JsonData(int delay, List itemIds, int operator, String altitude, int furniSource) { + this.delay = delay; + this.itemIds = itemIds; + this.operator = operator; + this.altitude = altitude; + this.furniSource = furniSource; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTeleport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTeleport.java index 03ecbf13..92c60e46 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTeleport.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTeleport.java @@ -7,16 +7,11 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.pets.RideablePet; -import com.eu.habbo.habbohotel.rooms.Room; -import com.eu.habbo.habbohotel.rooms.RoomTile; -import com.eu.habbo.habbohotel.rooms.RoomTileState; -import com.eu.habbo.habbohotel.rooms.RoomUnit; -import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; -import com.eu.habbo.habbohotel.rooms.RoomUnitType; +import com.eu.habbo.habbohotel.rooms.*; import com.eu.habbo.habbohotel.users.Habbo; -import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; @@ -39,6 +34,7 @@ public class WiredEffectTeleport extends InteractionWiredEffect { public static final WiredEffectType type = WiredEffectType.TELEPORT; protected List items; + private boolean fastTeleport = false; private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; private int userSource = WiredSourceUtil.SOURCE_TRIGGER; @@ -53,6 +49,10 @@ public class WiredEffectTeleport extends InteractionWiredEffect { } public static void teleportUnitToTile(RoomUnit roomUnit, RoomTile tile) { + teleportUnitToTile(roomUnit, tile, false); + } + + public static void teleportUnitToTile(RoomUnit roomUnit, RoomTile tile, boolean fastTeleport) { if (roomUnit == null || tile == null || roomUnit.isWiredTeleporting) return; @@ -85,9 +85,10 @@ public class WiredEffectTeleport extends InteractionWiredEffect { // makes a temporary effect + int teleportDelay = getTeleportDelay(fastTeleport); + roomUnit.getRoom().unIdle(roomUnit.getRoom().getHabbo(roomUnit)); - room.sendComposer(new RoomUserEffectComposer(roomUnit, 4).compose()); - Emulator.getThreading().run(new SendRoomUnitEffectComposer(room, roomUnit), WiredManager.TELEPORT_DELAY + 1000); + sendTeleportEffect(room, roomUnit, fastTeleport); if (tile == roomUnit.getCurrentLocation()) { return; @@ -110,8 +111,8 @@ public class WiredEffectTeleport extends InteractionWiredEffect { } } - Emulator.getThreading().run(() -> { roomUnit.isWiredTeleporting = true; }, Math.max(0, WiredManager.TELEPORT_DELAY - 500)); - Emulator.getThreading().run(new RoomUnitTeleport(roomUnit, room, tile.x, tile.y, tile.getStackHeight() + (tile.state == RoomTileState.SIT ? -0.5 : 0), roomUnit.getEffectId()), WiredManager.TELEPORT_DELAY); + Emulator.getThreading().run(() -> { roomUnit.isWiredTeleporting = true; }, Math.max(0, teleportDelay - 500)); + Emulator.getThreading().run(new RoomUnitTeleport(roomUnit, room, tile.x, tile.y, tile.getStackHeight() + (tile.state == RoomTileState.SIT ? -0.5 : 0), roomUnit.getEffectId()), teleportDelay); } @Override @@ -137,7 +138,8 @@ public class WiredEffectTeleport extends InteractionWiredEffect { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(2); + message.appendInt(3); + message.appendInt(this.fastTeleport ? 1 : 0); message.appendInt(this.furniSource); message.appendInt(this.userSource); message.appendInt(0); @@ -166,8 +168,15 @@ public class WiredEffectTeleport extends InteractionWiredEffect { @Override public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { int[] params = settings.getIntParams(); - this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; - this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + if (params.length > 2) { + this.fastTeleport = params[0] == 1; + this.furniSource = params[1]; + this.userSource = params[2]; + } else { + this.fastTeleport = false; + this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + } int itemsCount = settings.getFurniIds().length; @@ -175,6 +184,10 @@ public class WiredEffectTeleport extends InteractionWiredEffect { throw new WiredSaveException("Too many furni selected"); } + if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + List newItems = new ArrayList<>(); if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { @@ -227,7 +240,7 @@ public class WiredEffectTeleport extends InteractionWiredEffect { RoomTile tile = room.getLayout().getTile(item.getX(), item.getY()); if (tile != null) { - teleportUnitToTile(roomUnit, tile); + teleportUnitToTile(roomUnit, tile, this.fastTeleport); } } } @@ -244,6 +257,7 @@ public class WiredEffectTeleport extends InteractionWiredEffect { return WiredManager.getGson().toJson(new JsonData( this.getDelay(), itemsSnapshot.stream().map(HabboItem::getId).collect(Collectors.toList()), + this.fastTeleport, this.furniSource, this.userSource )); @@ -257,6 +271,7 @@ public class WiredEffectTeleport extends InteractionWiredEffect { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.setDelay(data.delay); + this.fastTeleport = data.fastTeleport; this.furniSource = data.furniSource; this.userSource = data.userSource; for (Integer id: data.itemIds) { @@ -284,6 +299,7 @@ public class WiredEffectTeleport extends InteractionWiredEffect { } } } + this.fastTeleport = false; this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; } @@ -292,6 +308,7 @@ public class WiredEffectTeleport extends InteractionWiredEffect { @Override public void onPickUp() { this.items.clear(); + this.fastTeleport = false; this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; this.setDelay(0); @@ -312,15 +329,30 @@ public class WiredEffectTeleport extends InteractionWiredEffect { return COOLDOWN_DEFAULT; } + private static int getTeleportDelay(boolean fastTeleport) { + return fastTeleport ? Math.max(75, WiredManager.TELEPORT_DELAY / 5) : WiredManager.TELEPORT_DELAY; + } + + private static int getTeleportEffectDuration(boolean fastTeleport) { + return fastTeleport ? Math.max(300, (WiredManager.TELEPORT_DELAY + 1000) / 3) : (WiredManager.TELEPORT_DELAY + 1000); + } + + private static void sendTeleportEffect(Room room, RoomUnit roomUnit, boolean fastTeleport) { + room.sendComposer(new RoomUserEffectComposer(roomUnit, 4).compose()); + Emulator.getThreading().run(new SendRoomUnitEffectComposer(room, roomUnit), getTeleportEffectDuration(fastTeleport)); + } + static class JsonData { int delay; List itemIds; + boolean fastTeleport; int furniSource; int userSource; - public JsonData(int delay, List itemIds, int furniSource, int userSource) { + public JsonData(int delay, List itemIds, boolean fastTeleport, int furniSource, int userSource) { this.delay = delay; this.itemIds = itemIds; + this.fastTeleport = fastTeleport; this.furniSource = furniSource; this.userSource = userSource; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectToggleFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectToggleFurni.java index 09a14d67..6aa3a078 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectToggleFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectToggleFurni.java @@ -39,10 +39,13 @@ import java.util.stream.Collectors; public class WiredEffectToggleFurni extends InteractionWiredEffect { private static final Logger LOGGER = LoggerFactory.getLogger(WiredEffectToggleFurni.class); + private static final int TOGGLE_TYPE_NEXT = 0; + private static final int TOGGLE_TYPE_PREVIOUS = 1; public static final WiredEffectType type = WiredEffectType.TOGGLE_STATE; private final THashSet items; + private int toggleType = TOGGLE_TYPE_NEXT; private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; private static final List> FORBIDDEN_TYPES = new ArrayList>() { @@ -122,7 +125,8 @@ public class WiredEffectToggleFurni extends InteractionWiredEffect { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(1); + message.appendInt(2); + message.appendInt(this.toggleType); message.appendInt(this.furniSource); message.appendInt(0); message.appendInt(this.getType().code); @@ -151,7 +155,13 @@ public class WiredEffectToggleFurni extends InteractionWiredEffect { @Override public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { int[] params = settings.getIntParams(); - this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + if (params.length > 1) { + this.toggleType = normalizeToggleType(params[0]); + this.furniSource = params[1]; + } else { + this.toggleType = TOGGLE_TYPE_NEXT; + this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + } int itemsCount = settings.getFurniIds().length; @@ -159,6 +169,10 @@ public class WiredEffectToggleFurni extends InteractionWiredEffect { throw new WiredSaveException("Too many furni selected"); } + if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + List newItems = new ArrayList<>(); if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { for (int i = 0; i < itemsCount; i++) { @@ -204,15 +218,7 @@ public class WiredEffectToggleFurni extends InteractionWiredEffect { try { if (item.getBaseItem().getStateCount() > 1 || item instanceof InteractionGameTimer) { - int state = 0; - if (!item.getExtradata().isEmpty()) { - try { - state = Integer.parseInt(item.getExtradata()); // assumes that extradata is state, could be something else for trophies etc. - } catch (NumberFormatException ignored) { - - } - } - item.onClick(habbo != null && !(item instanceof InteractionGameTimer) ? habbo.getClient() : null, room, new Object[]{state, this.getType()}); + this.toggleItemState(room, habbo, item); } } catch (Exception e) { LOGGER.error("Caught exception", e); @@ -235,6 +241,7 @@ public class WiredEffectToggleFurni extends InteractionWiredEffect { return WiredManager.getGson().toJson(new JsonData( this.getDelay(), new ArrayList<>(this.items).stream().map(HabboItem::getId).collect(Collectors.toList()), + this.toggleType, this.furniSource )); } @@ -247,6 +254,7 @@ public class WiredEffectToggleFurni extends InteractionWiredEffect { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.setDelay(data.delay); + this.toggleType = normalizeToggleType(data.toggleType); this.furniSource = data.furniSource; for (Integer id: data.itemIds) { HabboItem item = room.getHabboItem(id); @@ -281,6 +289,7 @@ public class WiredEffectToggleFurni extends InteractionWiredEffect { } } } + this.toggleType = TOGGLE_TYPE_NEXT; this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; } } @@ -288,6 +297,7 @@ public class WiredEffectToggleFurni extends InteractionWiredEffect { @Override public void onPickUp() { this.items.clear(); + this.toggleType = TOGGLE_TYPE_NEXT; this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; this.setDelay(0); } @@ -297,14 +307,52 @@ public class WiredEffectToggleFurni extends InteractionWiredEffect { return type; } + private int normalizeToggleType(int value) { + return (value == TOGGLE_TYPE_PREVIOUS) ? TOGGLE_TYPE_PREVIOUS : TOGGLE_TYPE_NEXT; + } + + private void toggleItemState(Room room, Habbo habbo, HabboItem item) throws Exception { + if (item.getBaseItem().getStateCount() <= 1) { + return; + } + + int stateCount = item.getBaseItem().getStateCount(); + int currentState = 0; + + if (!item.getExtradata().isEmpty()) { + try { + currentState = Integer.parseInt(item.getExtradata()); + } catch (NumberFormatException ignored) { + if (this.toggleType == TOGGLE_TYPE_NEXT) { + item.onClick(habbo != null ? habbo.getClient() : null, room, new Object[]{0, this.getType()}); + } + return; + } + } + + int nextState = (this.toggleType == TOGGLE_TYPE_PREVIOUS) + ? ((currentState - 1 + stateCount) % stateCount) + : ((currentState + 1) % stateCount); + + if (currentState == nextState) { + return; + } + + item.setExtradata(Integer.toString(nextState)); + item.needsUpdate(true); + room.updateItemState(item); + } + static class JsonData { int delay; List itemIds; + int toggleType; int furniSource; - public JsonData(int delay, List itemIds, int furniSource) { + public JsonData(int delay, List itemIds, int toggleType, int furniSource) { this.delay = delay; this.itemIds = itemIds; + this.toggleType = toggleType; this.furniSource = furniSource; } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectToggleRandom.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectToggleRandom.java index 370655af..32ba5870 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectToggleRandom.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectToggleRandom.java @@ -153,6 +153,10 @@ public class WiredEffectToggleRandom extends InteractionWiredEffect { throw new WiredSaveException("Too many furni selected"); } + if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + List newItems = new ArrayList<>(); if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { @@ -197,6 +201,7 @@ public class WiredEffectToggleRandom extends InteractionWiredEffect { try { item.setExtradata(Emulator.getRandom().nextInt(item.getBaseItem().getStateCount() + 1) + ""); + item.needsUpdate(true); room.updateItem(item); } catch (Exception e) { LOGGER.error("Caught exception", e); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTriggerStacks.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTriggerStacks.java index 7c07f009..2dda07c8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTriggerStacks.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTriggerStacks.java @@ -28,8 +28,8 @@ import java.util.stream.Collectors; public class WiredEffectTriggerStacks extends InteractionWiredEffect { public static final WiredEffectType type = WiredEffectType.CALL_STACKS; - private THashSet items; - private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + protected THashSet items; + protected int furniSource = WiredSourceUtil.SOURCE_TRIGGER; public WiredEffectTriggerStacks(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -101,6 +101,10 @@ public class WiredEffectTriggerStacks extends InteractionWiredEffect { throw new WiredSaveException("Too many furni selected"); } + if (itemsCount > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + List newItems = new ArrayList<>(); if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { @@ -132,7 +136,7 @@ public class WiredEffectTriggerStacks extends InteractionWiredEffect { /** * Maximum recursion depth to prevent infinite loops when trigger stacks call each other. */ - private static final int MAX_STACK_DEPTH = 10; + protected static final int MAX_STACK_DEPTH = 10; @Override public void execute(WiredContext ctx) { @@ -147,30 +151,8 @@ public class WiredEffectTriggerStacks extends InteractionWiredEffect { return; } - List effectiveItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + THashSet usedTiles = collectTargetTiles(room, ctx); - THashSet usedTiles = new THashSet<>(); - - for (HabboItem item : effectiveItems) { - if (item == null) continue; - - boolean found = false; - for (RoomTile tile : usedTiles) { - if (tile.x == item.getX() && tile.y == item.getY()) { - found = true; - break; - } - } - - if (!found) { - RoomTile tile = room.getLayout().getTile(item.getX(), item.getY()); - if (tile != null) { - usedTiles.add(tile); - } - } - } - - // Execute effects at tiles with incremented call stack depth WiredManager.executeEffectsAtTiles(usedTiles, roomUnit, room, currentDepth + 1); } @@ -246,6 +228,31 @@ public class WiredEffectTriggerStacks extends InteractionWiredEffect { return COOLDOWN_TRIGGER_STACKS; } + protected List resolveEffectiveItems(WiredContext ctx) { + return WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + } + + protected THashSet collectTargetTiles(Room room, WiredContext ctx) { + THashSet usedTiles = new THashSet<>(); + + if (room == null || room.getLayout() == null) { + return usedTiles; + } + + for (HabboItem item : resolveEffectiveItems(ctx)) { + if (item == null) { + continue; + } + + RoomTile tile = room.getLayout().getTile(item.getX(), item.getY()); + if (tile != null) { + usedTiles.add(tile); + } + } + + return usedTiles; + } + static class JsonData { int delay; List itemIds; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUnfreeze.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUnfreeze.java new file mode 100644 index 00000000..40978a4b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUnfreeze.java @@ -0,0 +1,149 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import gnu.trove.procedure.TObjectProcedure; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredEffectUnfreeze extends InteractionWiredEffect { + public static final WiredEffectType type = WiredEffectType.UNFREEZE; + + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredEffectUnfreeze(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectUnfreeze(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.userSource)) { + if (room.getHabbo(roomUnit) == null || !WiredFreezeUtil.isFrozen(roomUnit)) { + continue; + } + + WiredFreezeUtil.unfreeze(room, roomUnit); + } + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.getDelay(), this.userSource)); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + String wiredData = set.getString("wired_data"); + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.setDelay(data.delay); + this.userSource = data.userSource; + } else { + this.setDelay(0); + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + } + } + + @Override + public void onPickUp() { + this.setDelay(0); + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(1); + message.appendInt(this.userSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + + if (this.requiresTriggeringUser()) { + List invalidTriggers = new ArrayList<>(); + room.getRoomSpecialTypes().getTriggers(this.getX(), this.getY()).forEach(new TObjectProcedure() { + @Override + public boolean execute(InteractionWiredTrigger object) { + if (!object.isTriggeredByRoomUnit()) { + invalidTriggers.add(object.getBaseItem().getSpriteId()); + } + return true; + } + }); + message.appendInt(invalidTriggers.size()); + for (Integer i : invalidTriggers) { + message.appendInt(i); + } + } else { + message.appendInt(0); + } + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + int[] params = settings.getIntParams(); + this.userSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + + int delay = settings.getDelay(); + if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { + throw new WiredSaveException("Delay too long"); + } + + this.setDelay(delay); + return true; + } + + @Override + public boolean requiresTriggeringUser() { + return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; + } + + static class JsonData { + int delay; + int userSource; + + public JsonData(int delay, int userSource) { + this.delay = delay; + this.userSource = userSource; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserFurniBase.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserFurniBase.java new file mode 100644 index 00000000..359e463b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserFurniBase.java @@ -0,0 +1,330 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import com.eu.habbo.messages.outgoing.rooms.WiredMovementsComposer; +import gnu.trove.procedure.TObjectProcedure; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public abstract class WiredEffectUserFurniBase extends InteractionWiredEffect { + protected final List items = new ArrayList<>(); + protected int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int userSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredEffectUserFurniBase(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectUserFurniBase(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + protected List resolveItems(WiredContext ctx) { + Room room = ctx.room(); + List effectiveItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED && room != null) { + this.items.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + } + + return effectiveItems; + } + + protected HabboItem resolveLastItem(WiredContext ctx) { + List effectiveItems = this.resolveItems(ctx); + + if (effectiveItems.isEmpty()) { + return null; + } + + for (int index = effectiveItems.size() - 1; index >= 0; index--) { + HabboItem item = effectiveItems.get(index); + + if (item != null) { + return item; + } + } + + return null; + } + + protected Habbo resolveLastHabbo(Room room, WiredContext ctx) { + Habbo targetHabbo = null; + + for (RoomUnit unit : WiredSourceUtil.resolveUsers(ctx, this.userSource)) { + Habbo habbo = room.getHabbo(unit); + + if (habbo != null) { + targetHabbo = habbo; + } + } + + return targetHabbo; + } + + protected List resolveHabbos(Room room, WiredContext ctx) { + List habbos = new ArrayList<>(); + + for (RoomUnit unit : WiredSourceUtil.resolveUsers(ctx, this.userSource)) { + Habbo habbo = room.getHabbo(unit); + + if (habbo != null) { + habbos.add(habbo); + } + } + + return habbos; + } + + protected RoomTile resolveTargetTile(Habbo habbo) { + if (habbo == null || habbo.getRoomUnit() == null) { + return null; + } + + RoomUnit roomUnit = habbo.getRoomUnit(); + RoomTile movingTile = this.resolveActiveMoveTile(roomUnit); + + if (movingTile != null) { + return movingTile; + } + + return roomUnit.getCurrentLocation(); + } + + private RoomTile resolveActiveMoveTile(RoomUnit roomUnit) { + if (roomUnit == null || roomUnit.getRoom() == null || roomUnit.getRoom().getLayout() == null) { + return null; + } + + String moveStatus = roomUnit.getStatus(RoomUnitStatus.MOVE); + if (moveStatus != null && !moveStatus.isEmpty()) { + String[] parts = moveStatus.split(","); + if (parts.length >= 2) { + try { + return roomUnit.getRoom().getLayout().getTile( + Short.parseShort(parts[0]), + Short.parseShort(parts[1])); + } catch (NumberFormatException ignored) { + } + } + } + + return null; + } + + protected Integer resolveFollowAnimationDuration(Room room, Habbo habbo, HabboItem stackItem) { + if (room == null || habbo == null || habbo.getRoomUnit() == null || stackItem == null) { + return null; + } + + RoomUnit roomUnit = habbo.getRoomUnit(); + if (this.resolveActiveMoveTile(roomUnit) == null) { + return null; + } + + long moveStatusTimestamp = roomUnit.getMoveStatusTimestamp(); + if (moveStatusTimestamp <= 0L) { + return null; + } + + int configuredDuration = WiredMoveCarryHelper.getAnimationDuration(room, stackItem, WiredMovementsComposer.DEFAULT_DURATION); + int remainingStepDuration = (int) Math.max(50L, WiredMovementsComposer.DEFAULT_DURATION - Math.max(0L, System.currentTimeMillis() - moveStatusTimestamp)); + return Math.min(configuredDuration, remainingStepDuration); + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.getDelay(), + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), + this.furniSource, + this.userSource + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.items.clear(); + String wiredData = set.getString("wired_data"); + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.setDelay(data.delay); + this.furniSource = data.furniSource; + this.userSource = data.userSource; + + if (data.itemIds != null) { + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + + if (item != null) { + this.items.add(item); + } + } + } + + if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + } else { + String[] wiredDataOld = wiredData.split("\t"); + + if (wiredDataOld.length >= 1) { + this.setDelay(Integer.parseInt(wiredDataOld[0])); + } + + if (wiredDataOld.length == 2 && wiredDataOld[1].contains(";")) { + for (String s : wiredDataOld[1].split(";")) { + HabboItem item = room.getHabboItem(Integer.parseInt(s)); + + if (item != null) { + this.items.add(item); + } + } + } + + this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + } + } + + @Override + public void onPickUp() { + this.items.clear(); + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + List itemsSnapshot = new ArrayList<>(this.items); + itemsSnapshot.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + this.items.clear(); + this.items.addAll(itemsSnapshot); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(itemsSnapshot.size()); + for (HabboItem item : itemsSnapshot) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(2); + message.appendInt(this.furniSource); + message.appendInt(this.userSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + + if (this.requiresTriggeringUser()) { + List invalidTriggers = new ArrayList<>(); + room.getRoomSpecialTypes().getTriggers(this.getX(), this.getY()).forEach(new TObjectProcedure() { + @Override + public boolean execute(InteractionWiredTrigger object) { + if (!object.isTriggeredByRoomUnit()) { + invalidTriggers.add(object.getId()); + } + return true; + } + }); + message.appendInt(invalidTriggers.size()); + for (Integer i : invalidTriggers) { + message.appendInt(i); + } + } else { + message.appendInt(0); + } + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + this.furniSource = (settings.getIntParams().length > 0) ? settings.getIntParams()[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.userSource = (settings.getIntParams().length > 1) ? settings.getIntParams()[1] : WiredSourceUtil.SOURCE_TRIGGER; + + if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + + if (settings.getFurniIds().length > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + List newItems = new ArrayList<>(); + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item == null) { + throw new WiredSaveException(String.format("Item %s not found", itemId)); + } + + newItems.add(item); + } + } + + int delay = settings.getDelay(); + if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { + throw new WiredSaveException("Delay too long"); + } + + this.items.clear(); + this.items.addAll(newItems); + this.setDelay(delay); + + return true; + } + + @Override + public boolean requiresTriggeringUser() { + return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; + } + + static class JsonData { + int delay; + List itemIds; + int furniSource; + int userSource; + + public JsonData(int delay, List itemIds, int furniSource, int userSource) { + this.delay = delay; + this.itemIds = itemIds; + this.furniSource = furniSource; + this.userSource = userSource; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserToFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserToFurni.java new file mode 100644 index 00000000..12b65a63 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserToFurni.java @@ -0,0 +1,336 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredMovementPhysics; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import gnu.trove.procedure.TObjectProcedure; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class WiredEffectUserToFurni extends WiredEffectUserFurniBase { + private static final int WALKMODE_IF_CLOSER = 0; + private static final int WALKMODE_CONTINUE = 1; + private static final int WALKMODE_STOP = 2; + + public static final WiredEffectType type = WiredEffectType.USER_TO_FURNI; + private int walkMode = WALKMODE_CONTINUE; + + public WiredEffectUserToFurni(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectUserToFurni(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + HabboItem item = this.resolveLastItem(ctx); + WiredMovementPhysics movementPhysics = WiredMoveCarryHelper.getUserMovementPhysics(room, this, ctx); + + if (room == null || item == null) { + return; + } + + RoomTile targetTile = room.getLayout().getTile(item.getX(), item.getY()); + if (targetTile == null) { + return; + } + + for (Habbo habbo : this.resolveHabbos(room, ctx)) { + this.moveHabboSmooth(room, habbo, item, targetTile, movementPhysics); + } + } + + @Deprecated + @Override + public boolean execute(com.eu.habbo.habbohotel.rooms.RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.getDelay(), + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), + this.furniSource, + this.userSource, + this.walkMode + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.items.clear(); + String wiredData = set.getString("wired_data"); + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.setDelay(data.delay); + this.furniSource = data.furniSource; + this.userSource = data.userSource; + this.walkMode = this.normalizeWalkMode((data.walkMode != null) ? data.walkMode : WALKMODE_CONTINUE); + + if (data.itemIds != null) { + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + + if (item != null) { + this.items.add(item); + } + } + } + + if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + + return; + } + + String[] wiredDataOld = wiredData.split("\t"); + + if (wiredDataOld.length >= 1) { + this.setDelay(Integer.parseInt(wiredDataOld[0])); + } + + if (wiredDataOld.length == 2 && wiredDataOld[1].contains(";")) { + for (String s : wiredDataOld[1].split(";")) { + HabboItem item = room.getHabboItem(Integer.parseInt(s)); + + if (item != null) { + this.items.add(item); + } + } + } + + this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.walkMode = WALKMODE_CONTINUE; + } + + @Override + public void onPickUp() { + super.onPickUp(); + this.walkMode = WALKMODE_CONTINUE; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + List itemsSnapshot = new ArrayList<>(this.items); + itemsSnapshot.removeIf(item -> item == null + || item.getRoomId() != this.getRoomId() + || room.getHabboItem(item.getId()) == null); + this.items.clear(); + this.items.addAll(itemsSnapshot); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(itemsSnapshot.size()); + for (HabboItem item : itemsSnapshot) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(3); + message.appendInt(this.furniSource); + message.appendInt(this.userSource); + message.appendInt(this.walkMode); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + + if (this.requiresTriggeringUser()) { + List invalidTriggers = new ArrayList<>(); + room.getRoomSpecialTypes().getTriggers(this.getX(), this.getY()).forEach(new TObjectProcedure() { + @Override + public boolean execute(InteractionWiredTrigger object) { + if (!object.isTriggeredByRoomUnit()) { + invalidTriggers.add(object.getBaseItem().getSpriteId()); + } + return true; + } + }); + message.appendInt(invalidTriggers.size()); + for (Integer i : invalidTriggers) { + message.appendInt(i); + } + } else { + message.appendInt(0); + } + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + this.furniSource = (settings.getIntParams().length > 0) ? settings.getIntParams()[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.userSource = (settings.getIntParams().length > 1) ? settings.getIntParams()[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.walkMode = this.normalizeWalkMode((settings.getIntParams().length > 2) ? settings.getIntParams()[2] : WALKMODE_CONTINUE); + + if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("Too many furni selected"); + } + + if (settings.getFurniIds().length > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + List newItems = new ArrayList<>(); + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item == null) { + throw new WiredSaveException(String.format("Item %s not found", itemId)); + } + + newItems.add(item); + } + } + + int delay = settings.getDelay(); + if (delay > Emulator.getConfig().getInt("hotel.wired.max_delay", 20)) { + throw new WiredSaveException("Delay too long"); + } + + this.items.clear(); + this.items.addAll(newItems); + this.setDelay(delay); + + return true; + } + + private void moveHabboSmooth(Room room, Habbo habbo, HabboItem item, RoomTile targetTile, WiredMovementPhysics movementPhysics) { + if (room == null || habbo == null || item == null || targetTile == null || habbo.getRoomUnit() == null) { + return; + } + + RoomUnit roomUnit = habbo.getRoomUnit(); + RoomTile oldLocation = roomUnit.getCurrentLocation(); + RoomTile previousGoal = roomUnit.getGoal(); + boolean wasWalking = roomUnit.isWalking(); + boolean noAnimation = WiredMoveCarryHelper.hasNoAnimationExtra(room, this); + + if (oldLocation == null) { + return; + } + + double newZ = item.getZ() + Item.getCurrentHeight(item); + int animationDuration = noAnimation ? 0 : WiredMoveCarryHelper.getAnimationDuration(room, this, WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION); + if (!WiredUserMovementHelper.moveUser(room, roomUnit, targetTile, newZ, + roomUnit.getBodyRotation(), roomUnit.getHeadRotation(), animationDuration, noAnimation, movementPhysics)) { + return; + } + + this.applyWalkMode(roomUnit, oldLocation, previousGoal, targetTile, wasWalking, + animationDuration); + roomUnit.setPreviousLocationZ(roomUnit.getZ()); + } + + private void applyWalkMode(RoomUnit roomUnit, RoomTile oldLocation, RoomTile previousGoal, RoomTile targetTile, boolean wasWalking, int delay) { + if (roomUnit == null || targetTile == null) { + return; + } + + Runnable applyGoal = () -> { + if (roomUnit.getCurrentLocation() == null + || roomUnit.isWalking() + || roomUnit.hasStatus(RoomUnitStatus.SIT) + || roomUnit.hasStatus(RoomUnitStatus.LAY) + || roomUnit.getCurrentLocation().x != targetTile.x + || roomUnit.getCurrentLocation().y != targetTile.y) { + return; + } + + if (this.walkMode == WALKMODE_STOP || !wasWalking || previousGoal == null) { + roomUnit.setGoalLocation(targetTile); + return; + } + + if (this.walkMode == WALKMODE_IF_CLOSER && !this.isCloserToGoal(oldLocation, targetTile, previousGoal)) { + roomUnit.setGoalLocation(targetTile); + return; + } + + roomUnit.setGoalLocation(previousGoal); + }; + + if (delay > 0) { + Emulator.getThreading().run(applyGoal, delay); + return; + } + + applyGoal.run(); + } + + private boolean isCloserToGoal(RoomTile oldLocation, RoomTile newLocation, RoomTile goalLocation) { + if (oldLocation == null || newLocation == null || goalLocation == null) { + return false; + } + + return this.distanceSquared(newLocation, goalLocation) < this.distanceSquared(oldLocation, goalLocation); + } + + private int distanceSquared(RoomTile first, RoomTile second) { + int dx = first.x - second.x; + int dy = first.y - second.y; + return (dx * dx) + (dy * dy); + } + + private int normalizeWalkMode(int walkMode) { + if (walkMode < WALKMODE_IF_CLOSER || walkMode > WALKMODE_STOP) { + return WALKMODE_CONTINUE; + } + + return walkMode; + } + + static class JsonData { + int delay; + List itemIds; + int furniSource; + int userSource; + Integer walkMode; + + public JsonData(int delay, List itemIds, int furniSource, int userSource, int walkMode) { + this.delay = delay; + this.itemIds = itemIds; + this.furniSource = furniSource; + this.userSource = userSource; + this.walkMode = walkMode; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java index 9ac89433..35e957c4 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java @@ -13,6 +13,7 @@ import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTextPlaceholderUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserWhisperComposer; @@ -21,13 +22,23 @@ import gnu.trove.procedure.TObjectProcedure; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; public class WiredEffectWhisper extends InteractionWiredEffect { public static final WiredEffectType type = WiredEffectType.SHOW_MESSAGE; + protected static final int VISIBILITY_SOURCE_USERS = 0; + protected static final int VISIBILITY_ALL_ROOM_USERS = 1; + private static final long DELIVERY_DEDUP_TTL_MS = 60_000L; + private static final int DELIVERY_DEDUP_CLEANUP_THRESHOLD = 512; + private static final ConcurrentHashMap DELIVERY_DEDUP = new ConcurrentHashMap<>(); protected String message = ""; protected int userSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int visibilitySelection = VISIBILITY_SOURCE_USERS; + protected int bubbleStyle = RoomChatMessageBubbles.WIRED.getType(); public WiredEffectWhisper(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -45,8 +56,10 @@ public class WiredEffectWhisper extends InteractionWiredEffect { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(this.message); - message.appendInt(1); + message.appendInt(3); message.appendInt(this.userSource); + message.appendInt(this.visibilitySelection); + message.appendInt(this.bubbleStyle); message.appendInt(0); message.appendInt(type.code); message.appendInt(this.getDelay()); @@ -76,6 +89,10 @@ public class WiredEffectWhisper extends InteractionWiredEffect { String message = settings.getStringParam(); int[] params = settings.getIntParams(); this.userSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.visibilitySelection = (params.length > 1 && params[1] == VISIBILITY_ALL_ROOM_USERS) + ? VISIBILITY_ALL_ROOM_USERS + : VISIBILITY_SOURCE_USERS; + this.bubbleStyle = (params.length > 2) ? params[2] : RoomChatMessageBubbles.WIRED.getType(); if(gameClient.getHabbo() == null || !gameClient.getHabbo().hasPermission(Permission.ACC_SUPERWIRED)) { message = Emulator.getGameEnvironment().getWordFilter().filter(message, null); @@ -96,16 +113,106 @@ public class WiredEffectWhisper extends InteractionWiredEffect { return WiredSourceUtil.resolveUsers(ctx, this.userSource); } + protected List resolveRecipients(WiredContext ctx, List sourceUsers) { + Room room = ctx.room(); + LinkedHashMap recipients = new LinkedHashMap<>(); + + if (room == null) { + return Collections.emptyList(); + } + + if (this.visibilitySelection == VISIBILITY_ALL_ROOM_USERS) { + for (Habbo habbo : room.getCurrentHabbos().values()) { + addRecipient(recipients, habbo); + } + } else { + for (RoomUnit roomUnit : sourceUsers) { + addRecipient(recipients, room.getHabbo(roomUnit)); + } + } + + return new ArrayList<>(recipients.values()); + } + + protected Habbo resolveMessageSourceHabbo(WiredContext ctx, List sourceUsers) { + Room room = ctx.room(); + + if (room != null) { + for (RoomUnit roomUnit : sourceUsers) { + Habbo habbo = room.getHabbo(roomUnit); + if (habbo != null) { + return habbo; + } + } + } + + return (room == null) ? null : ctx.actor().map(roomUnit -> room.getHabbo(roomUnit)).orElse(null); + } + + protected String buildMessage(WiredContext ctx, Habbo referenceHabbo) { + String username = ""; + + if (referenceHabbo != null && referenceHabbo.getHabboInfo() != null) { + username = referenceHabbo.getHabboInfo().getUsername(); + } + + String msg = this.message + .replace("%user%", username) + .replace("%online_count%", Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "") + .replace("%room_count%", Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size() + ""); + + return WiredTextPlaceholderUtil.applyUsernamePlaceholders(ctx, msg); + } + + private void addRecipient(LinkedHashMap recipients, Habbo habbo) { + if (habbo == null || habbo.getHabboInfo() == null || habbo.getClient() == null) { + return; + } + + recipients.putIfAbsent(habbo.getHabboInfo().getId(), habbo); + } + + protected boolean shouldDeliverToRecipient(WiredContext ctx, Habbo habbo) { + if (ctx == null || habbo == null || habbo.getHabboInfo() == null) { + return true; + } + + long now = System.currentTimeMillis(); + cleanupDeliveryDedup(now); + + String deliveryKey = buildDeliveryKey(ctx, habbo); + + return DELIVERY_DEDUP.putIfAbsent(deliveryKey, now) == null; + } + + private String buildDeliveryKey(WiredContext ctx, Habbo habbo) { + return ctx.room().getId() + ":" + this.getId() + ":" + habbo.getHabboInfo().getId() + ":" + ctx.event().getCreatedAtMs(); + } + + private static void cleanupDeliveryDedup(long now) { + if (DELIVERY_DEDUP.size() < DELIVERY_DEDUP_CLEANUP_THRESHOLD) { + return; + } + + DELIVERY_DEDUP.entrySet().removeIf(entry -> (now - entry.getValue()) > DELIVERY_DEDUP_TTL_MS); + } + @Override public void execute(WiredContext ctx) { - Room room = ctx.room(); if (this.message.length() > 0) { - for (RoomUnit roomUnit : resolveUsers(ctx)) { - Habbo habbo = room.getHabbo(roomUnit); - if (habbo == null) continue; + List sourceUsers = resolveUsers(ctx); + List recipients = resolveRecipients(ctx, sourceUsers); + Habbo sharedSourceHabbo = (this.visibilitySelection == VISIBILITY_ALL_ROOM_USERS) + ? resolveMessageSourceHabbo(ctx, sourceUsers) + : null; - String msg = this.message.replace("%user%", habbo.getHabboInfo().getUsername()).replace("%online_count%", Emulator.getGameEnvironment().getHabboManager().getOnlineCount() + "").replace("%room_count%", Emulator.getGameEnvironment().getRoomManager().getActiveRooms().size() + ""); - habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(msg, habbo, habbo, RoomChatMessageBubbles.WIRED))); + for (Habbo habbo : recipients) { + if (!shouldDeliverToRecipient(ctx, habbo)) { + continue; + } + + String msg = buildMessage(ctx, (sharedSourceHabbo != null) ? sharedSourceHabbo : habbo); + habbo.getClient().sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(msg, habbo, habbo, RoomChatMessageBubbles.getBubble(this.bubbleStyle)))); if (habbo.getRoomUnit().isIdle()) { habbo.getRoomUnit().getRoom().unIdle(habbo); @@ -122,7 +229,7 @@ public class WiredEffectWhisper extends InteractionWiredEffect { @Override public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData(this.message, this.getDelay(), this.userSource)); + return WiredManager.getGson().toJson(new JsonData(this.message, this.getDelay(), this.userSource, this.visibilitySelection, this.bubbleStyle)); } @Override @@ -133,7 +240,11 @@ public class WiredEffectWhisper extends InteractionWiredEffect { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.setDelay(data.delay); this.message = data.message; - this.userSource = data.userSource; + this.userSource = (data.userSource != null) ? data.userSource : WiredSourceUtil.SOURCE_TRIGGER; + this.visibilitySelection = (data.visibilitySelection != null && data.visibilitySelection == VISIBILITY_ALL_ROOM_USERS) + ? VISIBILITY_ALL_ROOM_USERS + : VISIBILITY_SOURCE_USERS; + this.bubbleStyle = (data.bubbleStyle != null) ? data.bubbleStyle : RoomChatMessageBubbles.WIRED.getType(); } else { this.message = ""; @@ -144,6 +255,8 @@ public class WiredEffectWhisper extends InteractionWiredEffect { } this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.visibilitySelection = VISIBILITY_SOURCE_USERS; + this.bubbleStyle = RoomChatMessageBubbles.WIRED.getType(); this.needsUpdate(true); } } @@ -152,6 +265,8 @@ public class WiredEffectWhisper extends InteractionWiredEffect { public void onPickUp() { this.message = ""; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.visibilitySelection = VISIBILITY_SOURCE_USERS; + this.bubbleStyle = RoomChatMessageBubbles.WIRED.getType(); this.setDelay(0); } @@ -162,18 +277,22 @@ public class WiredEffectWhisper extends InteractionWiredEffect { @Override public boolean requiresTriggeringUser() { - return this.userSource == WiredSourceUtil.SOURCE_TRIGGER; + return (this.userSource == WiredSourceUtil.SOURCE_TRIGGER) || WiredTextPlaceholderUtil.requiresActor(this.getRoom(), this); } static class JsonData { String message; int delay; - int userSource; + Integer userSource; + Integer visibilitySelection; + Integer bubbleStyle; - public JsonData(String message, int delay, int userSource) { + public JsonData(String message, int delay, int userSource, int visibilitySelection, int bubbleStyle) { this.message = message; this.delay = delay; this.userSource = userSource; + this.visibilitySelection = visibilitySelection; + this.bubbleStyle = bubbleStyle; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraAnimationTime.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraAnimationTime.java new file mode 100644 index 00000000..2c9a1def --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraAnimationTime.java @@ -0,0 +1,125 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.rooms.WiredMovementsComposer; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraAnimationTime extends InteractionWiredExtra { + public static final int CODE = 60; + public static final int MIN_DURATION_MS = 50; + public static final int MAX_DURATION_MS = 2000; + + private int durationMs = WiredMovementsComposer.DEFAULT_DURATION; + + public WiredExtraAnimationTime(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraAnimationTime(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int value = (settings.getIntParams().length > 0) ? settings.getIntParams()[0] : this.durationMs; + + if (value == this.durationMs && settings.getStringParam() != null && !settings.getStringParam().isEmpty()) { + try { + value = Integer.parseInt(settings.getStringParam()); + } catch (NumberFormatException ignored) { + value = this.durationMs; + } + } + + this.durationMs = normalizeDuration(value); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.durationMs)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(1); + message.appendInt(this.durationMs); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.durationMs = normalizeDuration((data != null) ? data.durationMs : WiredMovementsComposer.DEFAULT_DURATION); + return; + } + + try { + this.durationMs = normalizeDuration(Integer.parseInt(wiredData)); + } catch (NumberFormatException ignored) { + this.durationMs = WiredMovementsComposer.DEFAULT_DURATION; + } + } + + @Override + public void onPickUp() { + this.durationMs = WiredMovementsComposer.DEFAULT_DURATION; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { + + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public int getDurationMs() { + return this.durationMs; + } + + private static int normalizeDuration(int value) { + return Math.max(MIN_DURATION_MS, Math.min(MAX_DURATION_MS, value)); + } + + static class JsonData { + int durationMs; + + JsonData(int durationMs) { + this.durationMs = durationMs; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraContextVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraContextVariable.java new file mode 100644 index 00000000..ce0ee961 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraContextVariable.java @@ -0,0 +1,132 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraContextVariable extends InteractionWiredExtra { + public static final int CODE = 84; + + private String variableName = ""; + private boolean hasValue = false; + + public WiredExtraContextVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraContextVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + int[] intParams = settings.getIntParams(); + String normalizedName = WiredVariableNameValidator.normalizeForSave(settings.getStringParam()); + + WiredVariableNameValidator.validateDefinitionName(room, this.getId(), normalizedName); + + this.variableName = normalizedName; + this.hasValue = (intParams.length > 0) && (intParams[0] == 1); + + WiredContextVariableSupport.broadcastDefinitions(room); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.variableName, this.hasValue)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableName); + message.appendInt(1); + message.appendInt(this.hasValue ? 1 : 0); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + this.hasValue = data.hasValue; + } + + return; + } + + this.variableName = WiredVariableNameValidator.normalizeLegacy(wiredData); + } + + @Override + public void onPickUp() { + this.variableName = ""; + this.hasValue = false; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getVariableName() { + return this.variableName; + } + + public boolean hasValue() { + return this.hasValue; + } + + static class JsonData { + String variableName; + boolean hasValue; + + JsonData(String variableName, boolean hasValue) { + this.variableName = variableName; + this.hasValue = hasValue; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraExecuteInOrder.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraExecuteInOrder.java new file mode 100644 index 00000000..d6023d4e --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraExecuteInOrder.java @@ -0,0 +1,78 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraExecuteInOrder extends InteractionWiredExtra { + public static final int CODE = 64; + + public WiredExtraExecuteInOrder(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraExecuteInOrder(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData()); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(0); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + } + + @Override + public void onPickUp() { + + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { + + } + + @Override + public boolean hasConfiguration() { + return true; + } + + static class JsonData { + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraExecutionLimit.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraExecutionLimit.java new file mode 100644 index 00000000..f616b855 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraExecutionLimit.java @@ -0,0 +1,204 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayDeque; +import java.util.Deque; + +public class WiredExtraExecutionLimit extends InteractionWiredExtra { + public static final int CODE = 65; + public static final int MIN_EXECUTIONS = 1; + public static final int MAX_EXECUTIONS = 100; + public static final int DEFAULT_EXECUTIONS = 1; + public static final int MIN_TIME_WINDOW_MS = 1000; + public static final int MAX_TIME_WINDOW_MS = 10000; + public static final int DEFAULT_TIME_WINDOW_MS = 1000; + public static final int TIME_WINDOW_STEP_MS = 500; + + private final Deque recentExecutionTimestamps = new ArrayDeque<>(); + private int maxExecutions = DEFAULT_EXECUTIONS; + private int timeWindowMs = DEFAULT_TIME_WINDOW_MS; + + public WiredExtraExecutionLimit(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraExecutionLimit(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] intParams = settings.getIntParams(); + int nextExecutions = (intParams.length > 0) ? intParams[0] : this.maxExecutions; + int nextTimeWindowMs = (intParams.length > 1) ? intParams[1] : this.timeWindowMs; + + this.maxExecutions = normalizeExecutions(nextExecutions); + this.timeWindowMs = normalizeTimeWindowMs(nextTimeWindowMs); + clearRuntimeState(); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.maxExecutions, this.timeWindowMs)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(2); + message.appendInt(this.maxExecutions); + message.appendInt(this.timeWindowMs); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.maxExecutions = normalizeExecutions(data.maxExecutions); + this.timeWindowMs = normalizeTimeWindowMs(data.timeWindowMs); + } + + return; + } + + String[] legacyData = wiredData.split(";"); + + try { + if (legacyData.length > 0) { + this.maxExecutions = normalizeExecutions(Integer.parseInt(legacyData[0])); + } + + if (legacyData.length > 1) { + this.timeWindowMs = normalizeTimeWindowMs(Integer.parseInt(legacyData[1])); + } + } catch (NumberFormatException ignored) { + this.maxExecutions = DEFAULT_EXECUTIONS; + this.timeWindowMs = DEFAULT_TIME_WINDOW_MS; + } + } + + @Override + public void onPickUp() { + this.maxExecutions = DEFAULT_EXECUTIONS; + this.timeWindowMs = DEFAULT_TIME_WINDOW_MS; + clearRuntimeState(); + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { + + } + + @Override + public void onMove(Room room, RoomTile oldLocation, RoomTile newLocation) { + super.onMove(room, oldLocation, newLocation); + clearRuntimeState(); + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public boolean tryAcquireExecutionSlot(long timestamp) { + synchronized (this.recentExecutionTimestamps) { + pruneExpiredTimestamps(timestamp); + + if (this.recentExecutionTimestamps.size() >= this.maxExecutions) { + return false; + } + + this.recentExecutionTimestamps.addLast(timestamp); + return true; + } + } + + public boolean canExecuteAt(long timestamp) { + synchronized (this.recentExecutionTimestamps) { + pruneExpiredTimestamps(timestamp); + return this.recentExecutionTimestamps.size() < this.maxExecutions; + } + } + + public int getMaxExecutions() { + return this.maxExecutions; + } + + public int getTimeWindowMs() { + return this.timeWindowMs; + } + + public void clearRuntimeState() { + synchronized (this.recentExecutionTimestamps) { + this.recentExecutionTimestamps.clear(); + } + } + + private void pruneExpiredTimestamps(long timestamp) { + while (!this.recentExecutionTimestamps.isEmpty() + && (timestamp - this.recentExecutionTimestamps.peekFirst()) >= this.timeWindowMs) { + this.recentExecutionTimestamps.removeFirst(); + } + } + + private static int normalizeExecutions(int value) { + return Math.max(MIN_EXECUTIONS, Math.min(MAX_EXECUTIONS, value)); + } + + private static int normalizeTimeWindowMs(int value) { + if (value < MIN_TIME_WINDOW_MS) { + return MIN_TIME_WINDOW_MS; + } + + if (value > MAX_TIME_WINDOW_MS) { + return MAX_TIME_WINDOW_MS; + } + + return Math.round(value / (float) TIME_WINDOW_STEP_MS) * TIME_WINDOW_STEP_MS; + } + + static class JsonData { + int maxExecutions; + int timeWindowMs; + + JsonData(int maxExecutions, int timeWindowMs) { + this.maxExecutions = maxExecutions; + this.timeWindowMs = timeWindowMs; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterFurni.java new file mode 100644 index 00000000..293b5fbf --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterFurni.java @@ -0,0 +1,123 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraFilterFurni extends InteractionWiredExtra { + public static final int CODE = 56; + private static final int MAX_FILTER_AMOUNT = 10000; + + private int amount = 0; + + public WiredExtraFilterFurni(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraFilterFurni(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int value = (settings.getIntParams().length > 0) ? settings.getIntParams()[0] : 0; + + if (value == 0 && settings.getStringParam() != null && !settings.getStringParam().isEmpty()) { + try { + value = Integer.parseInt(settings.getStringParam()); + } catch (NumberFormatException ignored) { + value = 0; + } + } + + this.amount = normalizeAmount(value); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.amount)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(1); + message.appendInt(this.amount); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.amount = normalizeAmount((data != null) ? data.amount : 0); + return; + } + + try { + this.amount = normalizeAmount(Integer.parseInt(wiredData)); + } catch (NumberFormatException ignored) { + this.amount = 0; + } + } + + @Override + public void onPickUp() { + this.amount = 0; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { + + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public int getAmount() { + return this.amount; + } + + private static int normalizeAmount(int value) { + return Math.max(0, Math.min(MAX_FILTER_AMOUNT, value)); + } + + static class JsonData { + int amount; + + JsonData(int amount) { + this.amount = amount; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterFurniByVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterFurniByVariable.java new file mode 100644 index 00000000..153294c6 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterFurniByVariable.java @@ -0,0 +1,28 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.items.Item; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraFilterFurniByVariable extends WiredExtraVariableFilterBase { + public static final int CODE = 78; + + public WiredExtraFilterFurniByVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraFilterFurniByVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + protected int getVariableTargetType() { + return TARGET_FURNI; + } + + @Override + protected int getCode() { + return CODE; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterUser.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterUser.java new file mode 100644 index 00000000..bdc9c6c6 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterUser.java @@ -0,0 +1,123 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraFilterUser extends InteractionWiredExtra { + public static final int CODE = 57; + private static final int MAX_FILTER_AMOUNT = 10000; + + private int amount = 0; + + public WiredExtraFilterUser(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraFilterUser(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int value = (settings.getIntParams().length > 0) ? settings.getIntParams()[0] : 0; + + if (value == 0 && settings.getStringParam() != null && !settings.getStringParam().isEmpty()) { + try { + value = Integer.parseInt(settings.getStringParam()); + } catch (NumberFormatException ignored) { + value = 0; + } + } + + this.amount = normalizeAmount(value); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.amount)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(1); + message.appendInt(this.amount); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.amount = normalizeAmount((data != null) ? data.amount : 0); + return; + } + + try { + this.amount = normalizeAmount(Integer.parseInt(wiredData)); + } catch (NumberFormatException ignored) { + this.amount = 0; + } + } + + @Override + public void onPickUp() { + this.amount = 0; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { + + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public int getAmount() { + return this.amount; + } + + private static int normalizeAmount(int value) { + return Math.max(0, Math.min(MAX_FILTER_AMOUNT, value)); + } + + static class JsonData { + int amount; + + JsonData(int amount) { + this.amount = amount; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterUsersByVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterUsersByVariable.java new file mode 100644 index 00000000..660627ee --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFilterUsersByVariable.java @@ -0,0 +1,28 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.items.Item; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraFilterUsersByVariable extends WiredExtraVariableFilterBase { + public static final int CODE = 77; + + public WiredExtraFilterUsersByVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraFilterUsersByVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + protected int getVariableTargetType() { + return TARGET_USER; + } + + @Override + protected int getCode() { + return CODE; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFurniVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFurniVariable.java new file mode 100644 index 00000000..7a2dcd7d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraFurniVariable.java @@ -0,0 +1,157 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraFurniVariable extends InteractionWiredExtra { + public static final int CODE = 71; + public static final int AVAILABILITY_ROOM_ACTIVE = 1; + public static final int AVAILABILITY_PERMANENT = 10; + + private String variableName = ""; + private boolean hasValue = false; + private int availability = AVAILABILITY_ROOM_ACTIVE; + + public WiredExtraFurniVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraFurniVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + int[] intParams = settings.getIntParams(); + String normalizedName = WiredVariableNameValidator.normalizeForSave(settings.getStringParam()); + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + WiredVariableNameValidator.validateDefinitionName(room, this.getId(), normalizedName); + + this.variableName = normalizedName; + this.hasValue = (intParams.length > 0) && (intParams[0] == 1); + this.availability = normalizeAvailability((intParams.length > 1) ? intParams[1] : AVAILABILITY_ROOM_ACTIVE); + + room.getFurniVariableManager().handleDefinitionUpdated(this); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.variableName, this.hasValue, this.availability)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableName); + message.appendInt(2); + message.appendInt(this.hasValue ? 1 : 0); + message.appendInt(this.availability); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + this.hasValue = data.hasValue; + this.availability = normalizeAvailability(data.availability); + } + + return; + } + + this.variableName = WiredVariableNameValidator.normalizeLegacy(wiredData); + } + + @Override + public void onPickUp() { + this.variableName = ""; + this.hasValue = false; + this.availability = AVAILABILITY_ROOM_ACTIVE; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getVariableName() { + return this.variableName; + } + + public boolean hasValue() { + return this.hasValue; + } + + public int getAvailability() { + return this.availability; + } + + public boolean isPermanentAvailability() { + return this.availability == AVAILABILITY_PERMANENT; + } + + private static int normalizeAvailability(int value) { + if (value == AVAILABILITY_PERMANENT) { + return AVAILABILITY_PERMANENT; + } + + return AVAILABILITY_ROOM_ACTIVE; + } + + static class JsonData { + String variableName; + boolean hasValue; + int availability; + + JsonData(String variableName, boolean hasValue, int availability) { + this.variableName = variableName; + this.hasValue = hasValue; + this.availability = availability; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraMoveCarryUsers.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraMoveCarryUsers.java new file mode 100644 index 00000000..401f5cb4 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraMoveCarryUsers.java @@ -0,0 +1,157 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraMoveCarryUsers extends InteractionWiredExtra { + public static final int CODE = 58; + public static final int MODE_DIRECTLY_ON_FURNI = 0; + public static final int MODE_SAME_TILE = 1; + public static final int SOURCE_ALL_ROOM_USERS = 900; + + private int carryMode = MODE_DIRECTLY_ON_FURNI; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredExtraMoveCarryUsers(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraMoveCarryUsers(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + + this.carryMode = this.normalizeCarryMode((params.length > 0) ? params[0] : MODE_DIRECTLY_ON_FURNI); + this.userSource = this.normalizeUserSource((params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER); + + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.carryMode, this.userSource)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(2); + message.appendInt(this.carryMode); + message.appendInt(this.userSource); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.carryMode = this.normalizeCarryMode(data.carryMode); + this.userSource = this.normalizeUserSource(data.userSource); + } + + return; + } + + String[] legacyData = wiredData.split("\t"); + if (legacyData.length > 0) { + try { + this.carryMode = this.normalizeCarryMode(Integer.parseInt(legacyData[0])); + } catch (NumberFormatException ignored) { + this.carryMode = MODE_DIRECTLY_ON_FURNI; + } + } + + if (legacyData.length > 1) { + try { + this.userSource = this.normalizeUserSource(Integer.parseInt(legacyData[1])); + } catch (NumberFormatException ignored) { + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + } + } + } + + @Override + public void onPickUp() { + this.carryMode = MODE_DIRECTLY_ON_FURNI; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { + + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public int getCarryMode() { + return this.carryMode; + } + + public int getUserSource() { + return this.userSource; + } + + private int normalizeCarryMode(int value) { + return (value == MODE_SAME_TILE) ? MODE_SAME_TILE : MODE_DIRECTLY_ON_FURNI; + } + + private int normalizeUserSource(int value) { + switch (value) { + case SOURCE_ALL_ROOM_USERS: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_TRIGGER: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + static class JsonData { + int carryMode; + int userSource; + + JsonData(int carryMode, int userSource) { + this.carryMode = carryMode; + this.userSource = userSource; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraMoveNoAnimation.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraMoveNoAnimation.java new file mode 100644 index 00000000..3149aae3 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraMoveNoAnimation.java @@ -0,0 +1,78 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraMoveNoAnimation extends InteractionWiredExtra { + public static final int CODE = 59; + + public WiredExtraMoveNoAnimation(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraMoveNoAnimation(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData()); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(0); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + } + + @Override + public void onPickUp() { + + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { + + } + + @Override + public boolean hasConfiguration() { + return true; + } + + static class JsonData { + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraMovePhysics.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraMovePhysics.java new file mode 100644 index 00000000..cd890418 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraMovePhysics.java @@ -0,0 +1,229 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraMovePhysics extends InteractionWiredExtra { + public static final int CODE = 61; + public static final int SOURCE_ALL_ROOM = 900; + + private boolean keepAltitude = false; + private boolean moveThroughFurni = false; + private boolean moveThroughUsers = false; + private boolean blockByFurni = false; + private int moveThroughFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + private int moveThroughUsersSource = WiredSourceUtil.SOURCE_TRIGGER; + private int blockByFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredExtraMovePhysics(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraMovePhysics(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + + this.keepAltitude = readFlag(params, 0); + this.moveThroughFurni = readFlag(params, 1); + this.moveThroughUsers = readFlag(params, 2); + this.blockByFurni = readFlag(params, 3); + this.moveThroughFurniSource = normalizeSource(readInt(params, 4, WiredSourceUtil.SOURCE_TRIGGER)); + this.blockByFurniSource = normalizeSource(readInt(params, 5, WiredSourceUtil.SOURCE_TRIGGER)); + this.moveThroughUsersSource = normalizeSource(readInt(params, 6, WiredSourceUtil.SOURCE_TRIGGER)); + + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.keepAltitude, + this.moveThroughFurni, + this.moveThroughUsers, + this.blockByFurni, + this.moveThroughFurniSource, + this.blockByFurniSource, + this.moveThroughUsersSource)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(7); + message.appendInt(this.keepAltitude ? 1 : 0); + message.appendInt(this.moveThroughFurni ? 1 : 0); + message.appendInt(this.moveThroughUsers ? 1 : 0); + message.appendInt(this.blockByFurni ? 1 : 0); + message.appendInt(this.moveThroughFurniSource); + message.appendInt(this.blockByFurniSource); + message.appendInt(this.moveThroughUsersSource); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.keepAltitude = data.keepAltitude; + this.moveThroughFurni = data.moveThroughFurni; + this.moveThroughUsers = data.moveThroughUsers; + this.blockByFurni = data.blockByFurni; + this.moveThroughFurniSource = normalizeSource(data.moveThroughFurniSource); + this.blockByFurniSource = normalizeSource(data.blockByFurniSource); + this.moveThroughUsersSource = normalizeSource(data.moveThroughUsersSource); + } + + return; + } + + String[] legacyData = wiredData.split("\t"); + this.keepAltitude = readLegacyFlag(legacyData, 0); + this.moveThroughFurni = readLegacyFlag(legacyData, 1); + this.moveThroughUsers = readLegacyFlag(legacyData, 2); + this.blockByFurni = readLegacyFlag(legacyData, 3); + this.moveThroughFurniSource = normalizeSource(readLegacyInt(legacyData, 4, WiredSourceUtil.SOURCE_TRIGGER)); + this.blockByFurniSource = normalizeSource(readLegacyInt(legacyData, 5, WiredSourceUtil.SOURCE_TRIGGER)); + this.moveThroughUsersSource = normalizeSource(readLegacyInt(legacyData, 6, WiredSourceUtil.SOURCE_TRIGGER)); + } + + @Override + public void onPickUp() { + this.keepAltitude = false; + this.moveThroughFurni = false; + this.moveThroughUsers = false; + this.blockByFurni = false; + this.moveThroughFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.moveThroughUsersSource = WiredSourceUtil.SOURCE_TRIGGER; + this.blockByFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { + + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public boolean isKeepAltitude() { + return this.keepAltitude; + } + + public boolean isMoveThroughFurni() { + return this.moveThroughFurni; + } + + public boolean isMoveThroughUsers() { + return this.moveThroughUsers; + } + + public boolean isBlockByFurni() { + return this.blockByFurni; + } + + public int getMoveThroughFurniSource() { + return this.moveThroughFurniSource; + } + + public int getMoveThroughUsersSource() { + return this.moveThroughUsersSource; + } + + public int getBlockByFurniSource() { + return this.blockByFurniSource; + } + + private static boolean readFlag(int[] params, int index) { + return readInt(params, index, 0) == 1; + } + + private static int readInt(int[] params, int index, int fallback) { + return (params.length > index) ? params[index] : fallback; + } + + private static boolean readLegacyFlag(String[] data, int index) { + return readLegacyInt(data, index, 0) == 1; + } + + private static int readLegacyInt(String[] data, int index, int fallback) { + if (data.length <= index) { + return fallback; + } + + try { + return Integer.parseInt(data[index]); + } catch (NumberFormatException ignored) { + return fallback; + } + } + + private static int normalizeSource(int value) { + switch (value) { + case SOURCE_ALL_ROOM: + case WiredSourceUtil.SOURCE_TRIGGER: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + static class JsonData { + boolean keepAltitude; + boolean moveThroughFurni; + boolean moveThroughUsers; + boolean blockByFurni; + int moveThroughFurniSource; + int blockByFurniSource; + int moveThroughUsersSource; + + JsonData(boolean keepAltitude, boolean moveThroughFurni, boolean moveThroughUsers, boolean blockByFurni, int moveThroughFurniSource, int blockByFurniSource, int moveThroughUsersSource) { + this.keepAltitude = keepAltitude; + this.moveThroughFurni = moveThroughFurni; + this.moveThroughUsers = moveThroughUsers; + this.blockByFurni = blockByFurni; + this.moveThroughFurniSource = moveThroughFurniSource; + this.blockByFurniSource = blockByFurniSource; + this.moveThroughUsersSource = moveThroughUsersSource; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraOrEval.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraOrEval.java index 0f1feb67..548466b3 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraOrEval.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraOrEval.java @@ -1,21 +1,48 @@ package com.eu.habbo.habbohotel.items.interactions.wired.extra; +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; +import gnu.trove.set.hash.THashSet; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.List; +import java.util.stream.Collectors; public class WiredExtraOrEval extends InteractionWiredExtra { + public static final int CODE = 66; + public static final int MODE_ALL = 0; + public static final int MODE_AT_LEAST_ONE = 1; + public static final int MODE_NOT_ALL = 2; + public static final int MODE_NONE = 3; + public static final int MODE_LESS_THAN = 4; + public static final int MODE_EXACTLY = 5; + public static final int MODE_MORE_THAN = 6; + public static final int MIN_COMPARE_VALUE = 0; + public static final int MAX_COMPARE_VALUE = 100; + + private final THashSet items; + private int evaluationMode = MODE_ALL; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private int compareValue = 1; + public WiredExtraOrEval(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); + this.items = new THashSet<>(); } public WiredExtraOrEval(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { super(id, userId, item, extradata, limitedStack, limitedSells); + this.items = new THashSet<>(); } @Override @@ -23,28 +50,252 @@ public class WiredExtraOrEval extends InteractionWiredExtra { return false; } + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + + this.evaluationMode = normalizeEvaluationMode((params.length > 0) ? params[0] : MODE_ALL); + this.furniSource = normalizeFurniSource((params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER); + this.compareValue = normalizeCompareValue((params.length > 2) ? params[2] : this.compareValue); + this.items.clear(); + + if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { + return true; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + return false; + } + + if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + return false; + } + + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (isSelectableConditionOrExtra(item)) { + this.items.add(item); + } + } + + return true; + } + @Override public String getWiredData() { - return null; + return WiredManager.getGson().toJson(new JsonData( + this.evaluationMode, + this.furniSource, + this.compareValue, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) + )); } @Override public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(room); + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(this.items.size()); + + for (HabboItem item : this.items) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(3); + message.appendInt(this.evaluationMode); + message.appendInt(this.furniSource); + message.appendInt(this.compareValue); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); } @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.evaluationMode = normalizeEvaluationMode(data.evaluationMode); + this.furniSource = normalizeFurniSource(data.furniSource); + this.compareValue = normalizeCompareValue(data.compareValue); + + if (data.itemIds != null) { + for (Integer itemId : data.itemIds) { + HabboItem item = room.getHabboItem(itemId); + + if (isSelectableConditionOrExtra(item)) { + this.items.add(item); + } + } + } + } + + return; + } + + String[] legacyData = wiredData.split("[;\t]"); + + try { + if (legacyData.length > 0) { + this.evaluationMode = normalizeEvaluationMode(Integer.parseInt(legacyData[0])); + } + + if (legacyData.length > 1) { + this.furniSource = normalizeFurniSource(Integer.parseInt(legacyData[1])); + } + + if (legacyData.length > 2) { + this.compareValue = normalizeCompareValue(Integer.parseInt(legacyData[2])); + } + } catch (NumberFormatException ignored) { + this.evaluationMode = MODE_ALL; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.compareValue = 1; + } } @Override public void onPickUp() { - + this.items.clear(); + this.evaluationMode = MODE_ALL; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.compareValue = 1; } @Override public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { } + + @Override + public boolean hasConfiguration() { + return true; + } + + public int getEvaluationMode() { + return this.evaluationMode; + } + + public int getFurniSource() { + return this.furniSource; + } + + public int getCompareValue() { + return this.compareValue; + } + + private void refresh(Room room) { + THashSet remove = new THashSet<>(); + + for (HabboItem item : this.items) { + HabboItem roomItem = room.getHabboItem(item.getId()); + + if (!isSelectableConditionOrExtra(roomItem)) { + remove.add(item); + } + } + + for (HabboItem item : remove) { + this.items.remove(item); + } + } + + public static boolean matchesMode(int evaluationMode, int matchedRequirements, int totalRequirements, int compareValue) { + if (totalRequirements <= 0) { + return true; + } + + switch (normalizeEvaluationMode(evaluationMode)) { + case MODE_AT_LEAST_ONE: + return matchedRequirements > 0; + case MODE_NOT_ALL: + return matchedRequirements > 0 && matchedRequirements < totalRequirements; + case MODE_NONE: + return matchedRequirements == 0; + case MODE_LESS_THAN: + return matchedRequirements < normalizeCompareValue(compareValue); + case MODE_EXACTLY: + return matchedRequirements == normalizeCompareValue(compareValue); + case MODE_MORE_THAN: + return matchedRequirements > normalizeCompareValue(compareValue); + case MODE_ALL: + default: + return matchedRequirements >= totalRequirements; + } + } + + private static int normalizeEvaluationMode(int value) { + switch (value) { + case MODE_ALL: + case MODE_AT_LEAST_ONE: + case MODE_NOT_ALL: + case MODE_NONE: + case MODE_LESS_THAN: + case MODE_EXACTLY: + case MODE_MORE_THAN: + return value; + default: + return MODE_ALL; + } + } + + private static int normalizeCompareValue(int value) { + return Math.max(MIN_COMPARE_VALUE, Math.min(MAX_COMPARE_VALUE, value)); + } + + private static int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_TRIGGER: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + private static boolean isSelectableConditionOrExtra(HabboItem item) { + if (item == null || item.getBaseItem() == null || item.getBaseItem().getInteractionType() == null) { + return false; + } + + String interaction = item.getBaseItem().getInteractionType().getName(); + if (interaction == null) { + return false; + } + + String normalizedInteraction = interaction.toLowerCase(); + return normalizedInteraction.startsWith("wf_cnd_") || normalizedInteraction.startsWith("wf_xtra_"); + } + + static class JsonData { + int evaluationMode; + int furniSource; + int compareValue; + List itemIds; + + JsonData(int evaluationMode, int furniSource, int compareValue, List itemIds) { + this.evaluationMode = evaluationMode; + this.furniSource = furniSource; + this.compareValue = compareValue; + this.itemIds = itemIds; + } + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraRandom.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraRandom.java index 3a78591a..c79df874 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraRandom.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraRandom.java @@ -1,15 +1,41 @@ package com.eu.habbo.habbohotel.items.interactions.wired.extra; +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.api.IWiredEffect; +import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.ServerMessage; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.ToIntFunction; public class WiredExtraRandom extends InteractionWiredExtra { + public static final int CODE = 63; + private static final int DEFAULT_PICK_AMOUNT = 1; + private static final int DEFAULT_SKIP_EXECUTIONS = 0; + private static final int MAX_PICK_AMOUNT = 1000; + private static final int MAX_SKIP_EXECUTIONS = 1000; + + private final Deque> recentExecutionEffectIds = new ArrayDeque<>(); + + private int pickAmount = DEFAULT_PICK_AMOUNT; + private int skipExecutions = DEFAULT_SKIP_EXECUTIONS; + public WiredExtraRandom(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); } @@ -23,28 +49,192 @@ public class WiredExtraRandom extends InteractionWiredExtra { return false; } + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int resolvedPickAmount = (settings.getIntParams().length > 0) ? settings.getIntParams()[0] : DEFAULT_PICK_AMOUNT; + int resolvedSkipExecutions = (settings.getIntParams().length > 1) ? settings.getIntParams()[1] : DEFAULT_SKIP_EXECUTIONS; + + this.pickAmount = normalizePickAmount(resolvedPickAmount); + this.skipExecutions = normalizeSkipExecutions(resolvedSkipExecutions); + this.clearRecentExecutions(); + return true; + } + @Override public String getWiredData() { - return null; + return WiredManager.getGson().toJson(new JsonData(this.pickAmount, this.skipExecutions)); } @Override public void serializeWiredData(ServerMessage message, Room room) { - + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(2); + message.appendInt(this.pickAmount); + message.appendInt(this.skipExecutions); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); } @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.pickAmount = normalizePickAmount((data != null) ? data.pickAmount : DEFAULT_PICK_AMOUNT); + this.skipExecutions = normalizeSkipExecutions((data != null) ? data.skipExecutions : DEFAULT_SKIP_EXECUTIONS); + return; + } } @Override public void onPickUp() { - + this.pickAmount = DEFAULT_PICK_AMOUNT; + this.skipExecutions = DEFAULT_SKIP_EXECUTIONS; + this.clearRecentExecutions(); } @Override public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { } + + @Override + public boolean hasConfiguration() { + return true; + } + + @Override + public void onMove(Room room, com.eu.habbo.habbohotel.rooms.RoomTile oldLocation, com.eu.habbo.habbohotel.rooms.RoomTile newLocation) { + super.onMove(room, oldLocation, newLocation); + this.clearRecentExecutions(); + } + + public List selectEffects(List effects) { + return this.selectRandomEffects(effects, InteractionWiredEffect::getId); + } + + public List selectWiredEffects(List effects) { + return this.selectRandomEffects(effects, effect -> { + if (effect instanceof InteractionWiredEffect) { + return ((InteractionWiredEffect) effect).getId(); + } + + return System.identityHashCode(effect); + }); + } + + public int getPickAmount() { + return this.pickAmount; + } + + public int getSkipExecutions() { + return this.skipExecutions; + } + + private synchronized List selectRandomEffects(List effects, ToIntFunction idResolver) { + if (effects == null || effects.isEmpty()) { + return Collections.emptyList(); + } + + List shuffledEffects = new ArrayList<>(effects); + Collections.shuffle(shuffledEffects, Emulator.getRandom()); + + int desiredAmount = Math.min(this.pickAmount, shuffledEffects.size()); + Set recentEffectIds = this.getRecentEffectIds(); + LinkedHashSet selectedEffects = new LinkedHashSet<>(); + + for (T effect : shuffledEffects) { + if (recentEffectIds.contains(idResolver.applyAsInt(effect))) { + continue; + } + + selectedEffects.add(effect); + if (selectedEffects.size() >= desiredAmount) { + break; + } + } + + if (selectedEffects.size() < desiredAmount) { + for (T effect : shuffledEffects) { + selectedEffects.add(effect); + if (selectedEffects.size() >= desiredAmount) { + break; + } + } + } + + this.recordExecution(selectedEffects, idResolver); + return new ArrayList<>(selectedEffects); + } + + private synchronized void clearRecentExecutions() { + this.recentExecutionEffectIds.clear(); + } + + private Set getRecentEffectIds() { + LinkedHashSet ids = new LinkedHashSet<>(); + + if (this.skipExecutions <= 0) { + return ids; + } + + for (List executionIds : this.recentExecutionEffectIds) { + ids.addAll(executionIds); + } + + return ids; + } + + private void recordExecution(Collection selectedEffects, ToIntFunction idResolver) { + if (this.skipExecutions <= 0) { + this.recentExecutionEffectIds.clear(); + return; + } + + List executionIds = new ArrayList<>(); + if (selectedEffects != null) { + for (T effect : selectedEffects) { + if (effect != null) { + executionIds.add(idResolver.applyAsInt(effect)); + } + } + } + + this.recentExecutionEffectIds.addLast(executionIds); + + while (this.recentExecutionEffectIds.size() > this.skipExecutions) { + this.recentExecutionEffectIds.removeFirst(); + } + } + + private static int normalizePickAmount(int value) { + return Math.max(1, Math.min(MAX_PICK_AMOUNT, value)); + } + + private static int normalizeSkipExecutions(int value) { + return Math.max(0, Math.min(MAX_SKIP_EXECUTIONS, value)); + } + + static class JsonData { + int pickAmount; + int skipExecutions; + + JsonData(int pickAmount, int skipExecutions) { + this.pickAmount = pickAmount; + this.skipExecutions = skipExecutions; + } + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraRoomVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraRoomVariable.java new file mode 100644 index 00000000..3481fc67 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraRoomVariable.java @@ -0,0 +1,159 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraRoomVariable extends InteractionWiredExtra { + public static final int CODE = 72; + public static final int AVAILABILITY_ROOM_ACTIVE = 1; + public static final int AVAILABILITY_PERMANENT = 10; + public static final int AVAILABILITY_SHARED = 11; + + private String variableName = ""; + private int availability = AVAILABILITY_ROOM_ACTIVE; + + public WiredExtraRoomVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraRoomVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + String normalizedName = WiredVariableNameValidator.normalizeForSave(settings.getStringParam()); + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + WiredVariableNameValidator.validateDefinitionName(room, this.getId(), normalizedName); + + int[] intParams = settings.getIntParams(); + + this.variableName = normalizedName; + this.availability = normalizeAvailability((intParams.length > 0) ? intParams[0] : AVAILABILITY_ROOM_ACTIVE); + + room.getRoomVariableManager().handleDefinitionUpdated(this); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.variableName, this.availability)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + int currentValue = (room != null) ? room.getRoomVariableManager().getCurrentValue(this.getId()) : 0; + + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableName); + message.appendInt(2); + message.appendInt(this.availability); + message.appendInt(currentValue); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + this.availability = normalizeAvailability(data.availability); + } + + return; + } + + this.variableName = WiredVariableNameValidator.normalizeLegacy(wiredData); + } + + @Override + public void onPickUp() { + this.variableName = ""; + this.availability = AVAILABILITY_ROOM_ACTIVE; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getVariableName() { + return this.variableName; + } + + public boolean hasValue() { + return true; + } + + public int getAvailability() { + return this.availability; + } + + public boolean isPermanentAvailability() { + return this.availability == AVAILABILITY_PERMANENT || this.availability == AVAILABILITY_SHARED; + } + + public boolean isSharedAvailability() { + return this.availability == AVAILABILITY_SHARED; + } + + private static int normalizeAvailability(int value) { + if (value == AVAILABILITY_PERMANENT || value == AVAILABILITY_SHARED) { + return value; + } + + return AVAILABILITY_ROOM_ACTIVE; + } + + static class JsonData { + String variableName; + int availability; + + JsonData(String variableName, int availability) { + this.variableName = variableName; + this.availability = availability; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java new file mode 100644 index 00000000..504abe1c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextInputVariable.java @@ -0,0 +1,271 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredVariableTextConnectorSupport; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.regex.Pattern; + +public class WiredExtraTextInputVariable extends InteractionWiredExtra { + public static final int CODE = 85; + public static final int DISPLAY_NUMERIC = 1; + public static final int DISPLAY_TEXTUAL = 2; + public static final String DEFAULT_CAPTURER_NAME = ""; + public static final int MAX_CAPTURER_NAME_LENGTH = 32; + + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + private static final Pattern WRAPPED_PLACEHOLDER_PATTERN = Pattern.compile("^#(.*)#$"); + + private int variableItemId = 0; + private String variableToken = ""; + private String capturerName = DEFAULT_CAPTURER_NAME; + private int displayType = DISPLAY_NUMERIC; + + public WiredExtraTextInputVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraTextInputVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + int[] intParams = settings.getIntParams(); + String[] stringData = splitStringData(settings.getStringParam()); + String nextVariableToken = normalizeVariableToken(stringData[0]); + int nextVariableItemId = getCustomItemId(nextVariableToken); + + if (nextVariableItemId <= 0) { + throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + } + + WiredVariableDefinitionInfo definitionInfo = WiredContextVariableSupport.getDefinitionInfo(room, nextVariableItemId); + if (definitionInfo == null || !definitionInfo.hasValue()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + this.variableItemId = nextVariableItemId; + this.variableToken = nextVariableToken; + this.capturerName = normalizeCapturerName(stringData[1]); + this.displayType = normalizeDisplayType((intParams.length > 0) ? intParams[0] : DISPLAY_NUMERIC); + + if (!canUseTextualDisplay(room, this.variableItemId)) { + this.displayType = DISPLAY_NUMERIC; + } + + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.variableToken, this.variableItemId, this.capturerName, this.displayType)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableToken + "\t" + this.capturerName); + message.appendInt(1); + message.appendInt(this.getDisplayType(room)); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.variableToken = normalizeVariableToken((data.variableToken != null) + ? data.variableToken + : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : "")); + this.variableItemId = getCustomItemId(this.variableToken); + this.capturerName = normalizeCapturerName(data.capturerName); + this.displayType = normalizeDisplayType(data.displayType); + } + + return; + } + + String[] legacyData = splitStringData(wiredData); + this.variableToken = normalizeVariableToken(legacyData[0]); + this.variableItemId = getCustomItemId(this.variableToken); + this.capturerName = normalizeCapturerName(legacyData[1]); + } + + @Override + public void onPickUp() { + this.variableItemId = 0; + this.variableToken = ""; + this.capturerName = DEFAULT_CAPTURER_NAME; + this.displayType = DISPLAY_NUMERIC; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public int getVariableItemId() { + return this.variableItemId; + } + + public String getVariableToken() { + return this.variableToken; + } + + public String getCapturerName() { + return this.capturerName; + } + + public String getPlaceholderToken() { + return this.capturerName.isEmpty() ? "" : "#" + this.capturerName + "#"; + } + + public int getDisplayType(Room room) { + return (this.displayType == DISPLAY_TEXTUAL && canUseTextualDisplay(room, this.variableItemId)) + ? DISPLAY_TEXTUAL + : DISPLAY_NUMERIC; + } + + public Integer resolveCapturedValue(Room room, String rawValue) { + String normalizedValue = rawValue != null ? rawValue.trim() : ""; + if (normalizedValue.isEmpty()) { + return null; + } + + if (this.getDisplayType(room) == DISPLAY_TEXTUAL) { + return WiredVariableTextConnectorSupport.toValue(room, this.variableItemId, normalizedValue); + } + + try { + return Integer.parseInt(normalizedValue); + } catch (NumberFormatException ignored) { + return null; + } + } + + private static boolean canUseTextualDisplay(Room room, int definitionItemId) { + WiredVariableDefinitionInfo definitionInfo = WiredContextVariableSupport.getDefinitionInfo(room, definitionItemId); + return definitionInfo != null && definitionInfo.hasValue() && definitionInfo.isTextConnected(); + } + + private static String[] splitStringData(String value) { + if (value == null) { + return new String[]{ "", DEFAULT_CAPTURER_NAME }; + } + + String[] parts = value.split("\t", -1); + if (parts.length == 1) { + return new String[]{ parts[0], DEFAULT_CAPTURER_NAME }; + } + + return new String[]{ parts[0], parts[1] }; + } + + private static int normalizeDisplayType(int value) { + return (value == DISPLAY_TEXTUAL) ? DISPLAY_TEXTUAL : DISPLAY_NUMERIC; + } + + private static String normalizeCapturerName(String value) { + if (value == null) { + return DEFAULT_CAPTURER_NAME; + } + + String normalized = value.trim().replace("\t", "").replace("\r", "").replace("\n", ""); + if (WRAPPED_PLACEHOLDER_PATTERN.matcher(normalized).matches()) { + normalized = normalized.substring(1, normalized.length() - 1).trim(); + } + + if (normalized.length() > MAX_CAPTURER_NAME_LENGTH) { + normalized = normalized.substring(0, MAX_CAPTURER_NAME_LENGTH); + } + + return normalized; + } + + private static String normalizeVariableToken(String value) { + String normalized = value == null ? "" : value.trim(); + if (normalized.isEmpty()) { + return ""; + } + + if (normalized.startsWith(CUSTOM_TOKEN_PREFIX)) { + return normalized; + } + + try { + int parsedValue = Integer.parseInt(normalized); + return parsedValue > 0 ? (CUSTOM_TOKEN_PREFIX + parsedValue) : ""; + } catch (NumberFormatException ignored) { + return ""; + } + } + + private static int getCustomItemId(String token) { + if (token == null || !token.startsWith(CUSTOM_TOKEN_PREFIX)) { + return 0; + } + + try { + return Integer.parseInt(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } catch (NumberFormatException ignored) { + return 0; + } + } + + static class JsonData { + String variableToken; + int variableItemId; + String capturerName; + int displayType; + + JsonData(String variableToken, int variableItemId, String capturerName, int displayType) { + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.capturerName = capturerName; + this.displayType = displayType; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputFurniName.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputFurniName.java new file mode 100644 index 00000000..a6c21d93 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputFurniName.java @@ -0,0 +1,294 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import gnu.trove.set.hash.THashSet; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class WiredExtraTextOutputFurniName extends InteractionWiredExtra { + public static final int CODE = 68; + public static final int TYPE_SINGLE = 1; + public static final int TYPE_MULTIPLE = 2; + public static final String DEFAULT_PLACEHOLDER_NAME = ""; + public static final String DEFAULT_DELIMITER = ", "; + public static final int MAX_PLACEHOLDER_NAME_LENGTH = 32; + public static final int MAX_DELIMITER_LENGTH = 16; + + private static final Pattern WRAPPED_PLACEHOLDER_PATTERN = Pattern.compile("^\\$\\((.*)\\)$"); + + private final THashSet items; + private String placeholderName = DEFAULT_PLACEHOLDER_NAME; + private int placeholderType = TYPE_SINGLE; + private String delimiter = DEFAULT_DELIMITER; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredExtraTextOutputFurniName(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + this.items = new THashSet<>(); + } + + public WiredExtraTextOutputFurniName(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + this.items = new THashSet<>(); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] intParams = settings.getIntParams(); + String[] stringData = splitStringData(settings.getStringParam()); + + this.placeholderType = normalizePlaceholderType((intParams.length > 0) ? intParams[0] : TYPE_SINGLE); + this.furniSource = normalizeFurniSource((intParams.length > 1) ? intParams[1] : WiredSourceUtil.SOURCE_TRIGGER); + this.placeholderName = normalizePlaceholderName(stringData[0]); + this.delimiter = normalizeDelimiter(stringData[1]); + this.items.clear(); + + if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { + return true; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + return false; + } + + if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + return false; + } + + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + this.items.add(item); + } + } + + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.placeholderName, + this.placeholderType, + this.delimiter, + this.furniSource, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) + )); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(room); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(this.items.size()); + + for (HabboItem item : this.items) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.placeholderName + "\t" + this.delimiter); + message.appendInt(2); + message.appendInt(this.placeholderType); + message.appendInt(this.furniSource); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.placeholderName = normalizePlaceholderName(data.placeholderName); + this.placeholderType = normalizePlaceholderType(data.placeholderType); + this.delimiter = normalizeDelimiter(data.delimiter); + this.furniSource = normalizeFurniSource(data.furniSource); + + if (data.itemIds != null) { + for (Integer itemId : data.itemIds) { + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + this.items.add(item); + } + } + } + } + + return; + } + + String[] legacyData = splitStringData(wiredData); + this.placeholderName = normalizePlaceholderName(legacyData[0]); + this.delimiter = normalizeDelimiter(legacyData[1]); + } + + @Override + public void onPickUp() { + this.items.clear(); + this.placeholderName = DEFAULT_PLACEHOLDER_NAME; + this.placeholderType = TYPE_SINGLE; + this.delimiter = DEFAULT_DELIMITER; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { + + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getPlaceholderName() { + return this.placeholderName; + } + + public String getPlaceholderToken() { + return this.placeholderName.isEmpty() ? "" : "$(" + this.placeholderName + ")"; + } + + public int getPlaceholderType() { + return this.placeholderType; + } + + public String getDelimiter() { + return this.delimiter; + } + + public int getFurniSource() { + return this.furniSource; + } + + public THashSet getItems() { + return this.items; + } + + private void refresh(Room room) { + THashSet remove = new THashSet<>(); + + for (HabboItem item : this.items) { + if (room.getHabboItem(item.getId()) == null) { + remove.add(item); + } + } + + for (HabboItem item : remove) { + this.items.remove(item); + } + } + + private static String[] splitStringData(String value) { + if (value == null) { + return new String[] { DEFAULT_PLACEHOLDER_NAME, DEFAULT_DELIMITER }; + } + + String[] parts = value.split("\t", -1); + + if (parts.length <= 1) { + return new String[] { value, DEFAULT_DELIMITER }; + } + + return new String[] { parts[0], parts[1] }; + } + + private static int normalizePlaceholderType(int value) { + return (value == TYPE_MULTIPLE) ? TYPE_MULTIPLE : TYPE_SINGLE; + } + + private static String normalizePlaceholderName(String value) { + if (value == null) { + return DEFAULT_PLACEHOLDER_NAME; + } + + String normalized = value.trim().replace("\t", "").replace("\r", "").replace("\n", ""); + if (WRAPPED_PLACEHOLDER_PATTERN.matcher(normalized).matches()) { + normalized = normalized.substring(2, normalized.length() - 1).trim(); + } + + if (normalized.length() > MAX_PLACEHOLDER_NAME_LENGTH) { + normalized = normalized.substring(0, MAX_PLACEHOLDER_NAME_LENGTH); + } + + return normalized; + } + + private static String normalizeDelimiter(String value) { + if (value == null) { + return DEFAULT_DELIMITER; + } + + String normalized = value.replace("\t", "").replace("\r", "").replace("\n", ""); + + if (normalized.length() > MAX_DELIMITER_LENGTH) { + normalized = normalized.substring(0, MAX_DELIMITER_LENGTH); + } + + return normalized; + } + + private static int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_TRIGGER: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + static class JsonData { + String placeholderName; + int placeholderType; + String delimiter; + int furniSource; + List itemIds; + + JsonData(String placeholderName, int placeholderType, String delimiter, int furniSource, List itemIds) { + this.placeholderName = placeholderName; + this.placeholderType = placeholderType; + this.delimiter = delimiter; + this.furniSource = furniSource; + this.itemIds = itemIds; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputUsername.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputUsername.java new file mode 100644 index 00000000..72fd5821 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputUsername.java @@ -0,0 +1,212 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.regex.Pattern; + +public class WiredExtraTextOutputUsername extends InteractionWiredExtra { + public static final int CODE = 67; + public static final int TYPE_SINGLE = 1; + public static final int TYPE_MULTIPLE = 2; + public static final String DEFAULT_PLACEHOLDER_NAME = ""; + public static final String DEFAULT_DELIMITER = ", "; + public static final int MAX_PLACEHOLDER_NAME_LENGTH = 32; + public static final int MAX_DELIMITER_LENGTH = 16; + + private static final Pattern WRAPPED_PLACEHOLDER_PATTERN = Pattern.compile("^\\$\\((.*)\\)$"); + + private String placeholderName = DEFAULT_PLACEHOLDER_NAME; + private int placeholderType = TYPE_SINGLE; + private String delimiter = DEFAULT_DELIMITER; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredExtraTextOutputUsername(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraTextOutputUsername(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] intParams = settings.getIntParams(); + String[] stringData = splitStringData(settings.getStringParam()); + + this.placeholderType = normalizePlaceholderType((intParams.length > 0) ? intParams[0] : TYPE_SINGLE); + this.userSource = normalizeUserSource((intParams.length > 1) ? intParams[1] : WiredSourceUtil.SOURCE_TRIGGER); + this.placeholderName = normalizePlaceholderName(stringData[0]); + this.delimiter = normalizeDelimiter(stringData[1]); + + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.placeholderName, this.placeholderType, this.delimiter, this.userSource)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.placeholderName + "\t" + this.delimiter); + message.appendInt(2); + message.appendInt(this.placeholderType); + message.appendInt(this.userSource); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.placeholderName = normalizePlaceholderName(data.placeholderName); + this.placeholderType = normalizePlaceholderType(data.placeholderType); + this.delimiter = normalizeDelimiter(data.delimiter); + this.userSource = normalizeUserSource(data.userSource); + } + + return; + } + + String[] legacyData = splitStringData(wiredData); + this.placeholderName = normalizePlaceholderName(legacyData[0]); + this.delimiter = normalizeDelimiter(legacyData[1]); + } + + @Override + public void onPickUp() { + this.placeholderName = DEFAULT_PLACEHOLDER_NAME; + this.placeholderType = TYPE_SINGLE; + this.delimiter = DEFAULT_DELIMITER; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { + + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getPlaceholderName() { + return this.placeholderName; + } + + public String getPlaceholderToken() { + return this.placeholderName.isEmpty() ? "" : "$(" + this.placeholderName + ")"; + } + + public int getPlaceholderType() { + return this.placeholderType; + } + + public String getDelimiter() { + return this.delimiter; + } + + public int getUserSource() { + return this.userSource; + } + + private static String[] splitStringData(String value) { + if (value == null) { + return new String[] { DEFAULT_PLACEHOLDER_NAME, DEFAULT_DELIMITER }; + } + + String[] parts = value.split("\t", -1); + + if (parts.length <= 1) { + return new String[] { value, DEFAULT_DELIMITER }; + } + + return new String[] { parts[0], parts[1] }; + } + + private static int normalizePlaceholderType(int value) { + return (value == TYPE_MULTIPLE) ? TYPE_MULTIPLE : TYPE_SINGLE; + } + + private static String normalizePlaceholderName(String value) { + if (value == null) { + return DEFAULT_PLACEHOLDER_NAME; + } + + String normalized = value.trim().replace("\t", "").replace("\r", "").replace("\n", ""); + if (WRAPPED_PLACEHOLDER_PATTERN.matcher(normalized).matches()) { + normalized = normalized.substring(2, normalized.length() - 1).trim(); + } + + if (normalized.length() > MAX_PLACEHOLDER_NAME_LENGTH) { + normalized = normalized.substring(0, MAX_PLACEHOLDER_NAME_LENGTH); + } + + return normalized; + } + + private static String normalizeDelimiter(String value) { + if (value == null) { + return DEFAULT_DELIMITER; + } + + String normalized = value.replace("\t", "").replace("\r", "").replace("\n", ""); + + if (normalized.length() > MAX_DELIMITER_LENGTH) { + normalized = normalized.substring(0, MAX_DELIMITER_LENGTH); + } + + return normalized; + } + + private static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + static class JsonData { + String placeholderName; + int placeholderType; + String delimiter; + int userSource; + + JsonData(String placeholderName, int placeholderType, String delimiter, int userSource) { + this.placeholderName = placeholderName; + this.placeholderType = placeholderType; + this.delimiter = delimiter; + this.userSource = userSource; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputVariable.java new file mode 100644 index 00000000..9e9c9beb --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraTextOutputVariable.java @@ -0,0 +1,544 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredInternalVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import gnu.trove.set.hash.THashSet; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class WiredExtraTextOutputVariable extends InteractionWiredExtra { + public static final int CODE = 80; + public static final int TARGET_USER = 0; + public static final int TARGET_FURNI = 1; + public static final int TARGET_CONTEXT = 2; + public static final int TARGET_ROOM = 3; + public static final int DISPLAY_NUMERIC = 1; + public static final int DISPLAY_TEXTUAL = 2; + public static final int TYPE_SINGLE = 1; + public static final int TYPE_MULTIPLE = 2; + public static final String DEFAULT_VARIABLE_TOKEN = ""; + public static final String DEFAULT_PLACEHOLDER_NAME = ""; + public static final String DEFAULT_DELIMITER = ", "; + public static final int MAX_PLACEHOLDER_NAME_LENGTH = 32; + public static final int MAX_DELIMITER_LENGTH = 16; + + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + private static final String INTERNAL_TOKEN_PREFIX = "internal:"; + private static final Pattern WRAPPED_PLACEHOLDER_PATTERN = Pattern.compile("^\\$\\((.*)\\)$"); + + private final THashSet items; + private int targetType = TARGET_USER; + private int displayType = DISPLAY_NUMERIC; + private int placeholderType = TYPE_SINGLE; + private int userSource = WiredSourceUtil.SOURCE_TRIGGER; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private int variableItemId = 0; + private String variableToken = DEFAULT_VARIABLE_TOKEN; + private String placeholderName = DEFAULT_PLACEHOLDER_NAME; + private String delimiter = DEFAULT_DELIMITER; + + public WiredExtraTextOutputVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + this.items = new THashSet<>(); + } + + public WiredExtraTextOutputVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + this.items = new THashSet<>(); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + int[] intParams = settings.getIntParams(); + String[] stringData = splitStringData(settings.getStringParam()); + int nextTargetType = normalizeTargetType((intParams.length > 0) ? intParams[0] : TARGET_USER); + String nextVariableToken = normalizeVariableToken(stringData[0]); + + if (nextVariableToken.isEmpty()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + } + + if (!isValidVariable(room, nextTargetType, nextVariableToken)) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + int nextFurniSource = normalizeFurniSource((intParams.length > 4) ? intParams[4] : WiredSourceUtil.SOURCE_TRIGGER); + this.items.clear(); + + if (nextTargetType == TARGET_FURNI && nextFurniSource == WiredSourceUtil.SOURCE_SELECTED) { + if (settings.getFurniIds().length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + this.items.add(item); + } + } + } + + this.targetType = nextTargetType; + this.setVariableToken(nextVariableToken); + this.displayType = normalizeDisplayType((intParams.length > 1) ? intParams[1] : DISPLAY_NUMERIC); + this.placeholderType = normalizePlaceholderType((intParams.length > 2) ? intParams[2] : TYPE_SINGLE); + this.userSource = normalizeUserSource((intParams.length > 3) ? intParams[3] : WiredSourceUtil.SOURCE_TRIGGER); + this.furniSource = nextFurniSource; + this.placeholderName = normalizePlaceholderName(stringData[1]); + this.delimiter = normalizeDelimiter(stringData[2]); + + if (!canUseTextualDisplay(room, this.targetType, this.variableToken)) { + this.displayType = DISPLAY_NUMERIC; + } + + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.targetType, + this.variableToken, + this.variableItemId, + this.displayType, + this.placeholderType, + this.userSource, + this.furniSource, + this.placeholderName, + this.delimiter, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) + )); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(room); + + List selectedItems = new ArrayList<>(); + if (this.targetType == TARGET_FURNI && this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + selectedItems.addAll(this.items); + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(selectedItems.size()); + + for (HabboItem item : selectedItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableToken + "\t" + this.placeholderName + "\t" + this.delimiter); + message.appendInt(5); + message.appendInt(this.targetType); + message.appendInt(this.displayType); + message.appendInt(this.placeholderType); + message.appendInt(this.userSource); + message.appendInt(this.furniSource); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.targetType = normalizeTargetType(data.targetType); + this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); + this.displayType = normalizeDisplayType(data.displayType); + this.placeholderType = normalizePlaceholderType(data.placeholderType); + this.userSource = normalizeUserSource(data.userSource); + this.furniSource = normalizeFurniSource(data.furniSource); + this.placeholderName = normalizePlaceholderName(data.placeholderName); + this.delimiter = normalizeDelimiter(data.delimiter); + + if (room != null && data.itemIds != null) { + for (Integer itemId : data.itemIds) { + if (itemId == null || itemId <= 0) { + continue; + } + + HabboItem item = room.getHabboItem(itemId); + if (item != null) { + this.items.add(item); + } + } + } + + if (room == null || !canUseTextualDisplay(room, this.targetType, this.variableToken)) { + this.displayType = DISPLAY_NUMERIC; + } + } + + return; + } + + String[] legacyData = splitStringData(wiredData); + this.setVariableToken(normalizeVariableToken(legacyData[0])); + this.placeholderName = normalizePlaceholderName(legacyData[1]); + this.delimiter = normalizeDelimiter(legacyData[2]); + } + + @Override + public void onPickUp() { + this.items.clear(); + this.targetType = TARGET_USER; + this.setVariableToken(DEFAULT_VARIABLE_TOKEN); + this.displayType = DISPLAY_NUMERIC; + this.placeholderType = TYPE_SINGLE; + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.placeholderName = DEFAULT_PLACEHOLDER_NAME; + this.delimiter = DEFAULT_DELIMITER; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public int getTargetType() { + return this.targetType; + } + + public String getVariableToken() { + return this.variableToken; + } + + public int getVariableItemId() { + return this.variableItemId; + } + + public int getDisplayType(Room room) { + return (this.displayType == DISPLAY_TEXTUAL && canUseTextualDisplay(room, this.targetType, this.variableToken)) + ? DISPLAY_TEXTUAL + : DISPLAY_NUMERIC; + } + + public int getPlaceholderType() { + return this.placeholderType; + } + + public String getPlaceholderName() { + return this.placeholderName; + } + + public String getPlaceholderToken() { + return this.placeholderName.isEmpty() ? "" : "$(" + this.placeholderName + ")"; + } + + public String getDelimiter() { + return this.delimiter; + } + + public int getUserSource() { + return this.userSource; + } + + public int getFurniSource() { + return this.furniSource; + } + + public THashSet getItems() { + return this.items; + } + + public boolean requiresActor() { + return this.targetType == TARGET_USER + && (this.userSource == WiredSourceUtil.SOURCE_TRIGGER || this.userSource == WiredSourceUtil.SOURCE_CLICKED_USER); + } + + public void refresh(Room room) { + THashSet remove = new THashSet<>(); + + for (HabboItem item : this.items) { + if (room == null || room.getHabboItem(item.getId()) == null) { + remove.add(item); + } + } + + for (HabboItem item : remove) { + this.items.remove(item); + } + } + + public static boolean isCustomVariableToken(String token) { + return token != null && token.startsWith(CUSTOM_TOKEN_PREFIX); + } + + public static boolean isInternalVariableToken(String token) { + return token != null && token.startsWith(INTERNAL_TOKEN_PREFIX); + } + + public static String getInternalVariableKey(String token) { + return isInternalVariableToken(token) ? WiredInternalVariableSupport.normalizeKey(token.substring(INTERNAL_TOKEN_PREFIX.length())) : ""; + } + + public static int getCustomItemId(String token) { + if (!isCustomVariableToken(token)) { + return 0; + } + + try { + return Integer.parseInt(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } catch (NumberFormatException ignored) { + return 0; + } + } + + private static String[] splitStringData(String value) { + if (value == null) { + return new String[]{ DEFAULT_VARIABLE_TOKEN, DEFAULT_PLACEHOLDER_NAME, DEFAULT_DELIMITER }; + } + + String[] parts = value.split("\t", -1); + if (parts.length == 1) { + return new String[]{ value, DEFAULT_PLACEHOLDER_NAME, DEFAULT_DELIMITER }; + } + + if (parts.length == 2) { + return new String[]{ parts[0], parts[1], DEFAULT_DELIMITER }; + } + + return new String[]{ parts[0], parts[1], parts[2] }; + } + + private static int normalizeTargetType(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + private static int normalizeDisplayType(int value) { + return (value == DISPLAY_TEXTUAL) ? DISPLAY_TEXTUAL : DISPLAY_NUMERIC; + } + + private static int normalizePlaceholderType(int value) { + return (value == TYPE_MULTIPLE) ? TYPE_MULTIPLE : TYPE_SINGLE; + } + + private static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private static int normalizeFurniSource(int value) { + return switch (value) { + case WiredSourceUtil.SOURCE_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + } + + private static String normalizePlaceholderName(String value) { + if (value == null) { + return DEFAULT_PLACEHOLDER_NAME; + } + + String normalized = value.trim().replace("\t", "").replace("\r", "").replace("\n", ""); + if (WRAPPED_PLACEHOLDER_PATTERN.matcher(normalized).matches()) { + normalized = normalized.substring(2, normalized.length() - 1).trim(); + } + + if (normalized.length() > MAX_PLACEHOLDER_NAME_LENGTH) { + normalized = normalized.substring(0, MAX_PLACEHOLDER_NAME_LENGTH); + } + + return normalized; + } + + private static String normalizeDelimiter(String value) { + if (value == null) { + return DEFAULT_DELIMITER; + } + + String normalized = value.replace("\t", "").replace("\r", "").replace("\n", ""); + if (normalized.length() > MAX_DELIMITER_LENGTH) { + normalized = normalized.substring(0, MAX_DELIMITER_LENGTH); + } + + return normalized; + } + + private static String normalizeVariableToken(String value) { + String normalized = (value == null) ? "" : value.trim(); + if (normalized.isEmpty()) { + return ""; + } + + if (isCustomVariableToken(normalized)) { + return normalized; + } + + if (isInternalVariableToken(normalized)) { + return INTERNAL_TOKEN_PREFIX + WiredInternalVariableSupport.normalizeKey(normalized.substring(INTERNAL_TOKEN_PREFIX.length())); + } + + try { + int parsedValue = Integer.parseInt(normalized); + return parsedValue > 0 ? (CUSTOM_TOKEN_PREFIX + parsedValue) : ""; + } catch (NumberFormatException ignored) { + return ""; + } + } + + private void setVariableToken(String token) { + this.variableToken = normalizeVariableToken(token); + this.variableItemId = getCustomItemId(this.variableToken); + } + + private static boolean canUseTextualDisplay(Room room, int targetType, String variableToken) { + if (room == null || !isCustomVariableToken(variableToken)) { + return false; + } + + int itemId = getCustomItemId(variableToken); + if (itemId <= 0) { + return false; + } + + return switch (targetType) { + case TARGET_USER -> { + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(itemId); + yield definition != null && definition.hasValue() && definition.isTextConnected(); + } + case TARGET_FURNI -> { + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(itemId); + yield definition != null && definition.hasValue() && definition.isTextConnected(); + } + case TARGET_CONTEXT -> { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, itemId); + yield definition != null && definition.hasValue() && definition.isTextConnected(); + } + case TARGET_ROOM -> { + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(itemId); + yield definition != null && definition.hasValue() && definition.isTextConnected(); + } + default -> false; + }; + } + + private static boolean isValidVariable(Room room, int targetType, String variableToken) { + if (room == null) { + return false; + } + + return switch (targetType) { + case TARGET_USER -> isInternalVariableToken(variableToken) + ? canUseUserInternalReference(getInternalVariableKey(variableToken)) + : isUserCustomValue(room, getCustomItemId(variableToken)); + case TARGET_FURNI -> isInternalVariableToken(variableToken) + ? canUseFurniInternalReference(getInternalVariableKey(variableToken)) + : isFurniCustomValue(room, getCustomItemId(variableToken)); + case TARGET_CONTEXT -> isInternalVariableToken(variableToken) + ? WiredInternalVariableSupport.canUseContextReference(getInternalVariableKey(variableToken)) + : isContextCustomValue(room, getCustomItemId(variableToken)); + case TARGET_ROOM -> isInternalVariableToken(variableToken) + ? canUseRoomInternalReference(getInternalVariableKey(variableToken)) + : isRoomCustomValue(room, getCustomItemId(variableToken)); + default -> false; + }; + } + + private static boolean isUserCustomValue(Room room, int itemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getUserVariableManager().getDefinitionInfo(itemId) : null; + return definition != null && definition.hasValue(); + } + + private static boolean isFurniCustomValue(Room room, int itemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getFurniVariableManager().getDefinitionInfo(itemId) : null; + return definition != null && definition.hasValue(); + } + + private static boolean isRoomCustomValue(Room room, int itemId) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getRoomVariableManager().getDefinitionInfo(itemId) : null; + return definition != null && definition.hasValue(); + } + + private static boolean isContextCustomValue(Room room, int itemId) { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, itemId); + return definition != null && definition.hasValue(); + } + + private static boolean canUseUserInternalReference(String key) { + return WiredInternalVariableSupport.canUseUserReference(key); + } + + private static boolean canUseFurniInternalReference(String key) { + return WiredInternalVariableSupport.canUseFurniReference(key); + } + + private static boolean canUseRoomInternalReference(String key) { + return WiredInternalVariableSupport.canUseRoomReference(key); + } + + static class JsonData { + int targetType; + String variableToken; + int variableItemId; + int displayType; + int placeholderType; + int userSource; + int furniSource; + String placeholderName; + String delimiter; + List itemIds; + + JsonData(int targetType, String variableToken, int variableItemId, int displayType, int placeholderType, int userSource, int furniSource, String placeholderName, String delimiter, List itemIds) { + this.targetType = targetType; + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.displayType = displayType; + this.placeholderType = placeholderType; + this.userSource = userSource; + this.furniSource = furniSource; + this.placeholderName = placeholderName; + this.delimiter = delimiter; + this.itemIds = itemIds; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraUnseen.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraUnseen.java index 127021db..c96d9415 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraUnseen.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraUnseen.java @@ -1,11 +1,15 @@ package com.eu.habbo.habbohotel.items.interactions.wired.extra; +import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.api.IWiredEffect; +import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.ServerMessage; import java.sql.ResultSet; @@ -16,6 +20,8 @@ import java.util.List; import java.util.Set; public class WiredExtraUnseen extends InteractionWiredExtra { + public static final int CODE = 62; + /** * Maximum number of effect IDs to track to prevent memory leaks. * When limit is reached, oldest entries are removed automatically. @@ -41,19 +47,34 @@ public class WiredExtraUnseen extends InteractionWiredExtra { return false; } + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + return true; + } + @Override public String getWiredData() { - return null; + return WiredManager.getGson().toJson(new JsonData()); } @Override public void serializeWiredData(ServerMessage message, Room room) { - + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(0); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); } @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { - + this.onPickUp(); } @Override @@ -108,6 +129,47 @@ public class WiredExtraUnseen extends InteractionWiredExtra { return effect; } } + + public List selectWiredEffects(List effects) { + synchronized (this.seenList) { + List unseenEffects = new ArrayList<>(); + + for (IWiredEffect effect : effects) { + if ((effect instanceof InteractionWiredEffect) + && !this.seenList.contains(((InteractionWiredEffect) effect).getId())) { + unseenEffects.add(effect); + } + } + + IWiredEffect effect = null; + if (!unseenEffects.isEmpty()) { + effect = unseenEffects.get(0); + } else { + this.seenList.clear(); + + if (!effects.isEmpty()) { + effect = effects.get(0); + } + } + + if (effect instanceof InteractionWiredEffect) { + if (this.seenList.size() >= MAX_SEEN_LIST_SIZE) { + Integer oldest = this.seenList.iterator().next(); + this.seenList.remove(oldest); + } + + this.seenList.add(((InteractionWiredEffect) effect).getId()); + } + + if (effect == null) { + return new ArrayList<>(); + } + + List selectedEffects = new ArrayList<>(); + selectedEffects.add(effect); + return selectedEffects; + } + } /** * Gets the current size of the seen list. @@ -127,4 +189,12 @@ public class WiredExtraUnseen extends InteractionWiredExtra { this.seenList.clear(); } } + + @Override + public boolean hasConfiguration() { + return true; + } + + static class JsonData { + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraUserVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraUserVariable.java new file mode 100644 index 00000000..f02a8571 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraUserVariable.java @@ -0,0 +1,162 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredExtraUserVariable extends InteractionWiredExtra { + public static final int CODE = 70; + public static final int AVAILABILITY_ROOM = 0; + public static final int AVAILABILITY_PERMANENT = 10; + public static final int AVAILABILITY_SHARED = 11; + + private String variableName = ""; + private boolean hasValue = false; + private int availability = AVAILABILITY_ROOM; + + public WiredExtraUserVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraUserVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + int[] intParams = settings.getIntParams(); + String normalizedName = WiredVariableNameValidator.normalizeForSave(settings.getStringParam()); + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + WiredVariableNameValidator.validateDefinitionName(room, this.getId(), normalizedName); + + this.variableName = normalizedName; + this.hasValue = (intParams.length > 0) && (intParams[0] == 1); + this.availability = normalizeAvailability((intParams.length > 1) ? intParams[1] : AVAILABILITY_ROOM); + + room.getUserVariableManager().handleDefinitionUpdated(this); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.variableName, this.hasValue, this.availability)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableName); + message.appendInt(2); + message.appendInt(this.hasValue ? 1 : 0); + message.appendInt(this.availability); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + this.hasValue = data.hasValue; + this.availability = normalizeAvailability(data.availability); + } + + return; + } + + this.variableName = WiredVariableNameValidator.normalizeLegacy(wiredData); + } + + @Override + public void onPickUp() { + this.variableName = ""; + this.hasValue = false; + this.availability = AVAILABILITY_ROOM; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getVariableName() { + return variableName; + } + + public boolean hasValue() { + return hasValue; + } + + public int getAvailability() { + return availability; + } + + public boolean isPermanentAvailability() { + return this.availability == AVAILABILITY_PERMANENT || this.availability == AVAILABILITY_SHARED; + } + + public boolean isSharedAvailability() { + return this.availability == AVAILABILITY_SHARED; + } + + private static int normalizeAvailability(int value) { + if (value == AVAILABILITY_PERMANENT || value == AVAILABILITY_SHARED) { + return value; + } + + return AVAILABILITY_ROOM; + } + + static class JsonData { + String variableName; + boolean hasValue; + int availability; + + JsonData(String variableName, boolean hasValue, int availability) { + this.variableName = variableName; + this.hasValue = hasValue; + this.availability = availability; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableEcho.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableEcho.java new file mode 100644 index 00000000..0dd43827 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableEcho.java @@ -0,0 +1,808 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.FurnitureMovementError; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomTileState; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomRightLevels; +import com.eu.habbo.habbohotel.rooms.RoomUserRotation; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.DanceType; +import com.eu.habbo.habbohotel.users.HabboGender; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; +import com.eu.habbo.habbohotel.wired.core.WiredInternalVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; +import com.eu.habbo.habbohotel.wired.core.WiredVariableLevelSystemSupport; +import com.eu.habbo.habbohotel.wired.core.WiredVariableTextConnectorSupport; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import com.eu.habbo.util.HotelDateTimeUtil; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.Locale; + +public class WiredExtraVariableEcho extends InteractionWiredExtra { + public static final int CODE = 83; + public static final int TARGET_USER = 0; + public static final int TARGET_FURNI = 1; + public static final int TARGET_ROOM = 3; + + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + private static final String INTERNAL_TOKEN_PREFIX = "internal:"; + private static final int DEFAULT_USER_AVAILABILITY = WiredExtraUserVariable.AVAILABILITY_ROOM; + private static final int DEFAULT_FURNI_AVAILABILITY = WiredExtraFurniVariable.AVAILABILITY_ROOM_ACTIVE; + private static final int DEFAULT_ROOM_AVAILABILITY = WiredExtraRoomVariable.AVAILABILITY_ROOM_ACTIVE; + + private String variableName = ""; + private int sourceTargetType = TARGET_USER; + private String sourceVariableToken = ""; + private int sourceVariableItemId = 0; + private String sourceVariableName = ""; + + public WiredExtraVariableEcho(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraVariableEcho(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + ConfigData config = parseConfigData(settings.getStringParam()); + int normalizedTargetType = normalizeTargetType(config.sourceTargetType); + String normalizedToken = normalizeVariableToken(config.sourceVariableToken, config.sourceVariableItemId); + int normalizedItemId = getCustomVariableItemId(normalizedToken); + SourceState sourceState = this.resolveSourceState(room, normalizedTargetType, normalizedToken, normalizedItemId); + + if (normalizedToken.isEmpty()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + } + + if (sourceState == null || !sourceState.hasValue()) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + if (!isAllowedEchoSource(sourceState, normalizedToken)) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + if (!WiredVariableTextConnectorSupport.isTextConnected(room, this)) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + if (createsCycle(room, this.getId(), normalizedTargetType, normalizedToken, normalizedItemId)) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + String normalizedName = deriveVariableName(config.variableName, sourceState.getName()); + WiredVariableNameValidator.validateDefinitionName(room, this.getId(), normalizedName); + + this.variableName = normalizedName; + this.sourceTargetType = normalizedTargetType; + this.sourceVariableToken = normalizedToken; + this.sourceVariableItemId = normalizedItemId; + this.sourceVariableName = sourceState.getName(); + + room.getUserVariableManager().broadcastSnapshot(); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.variableName, this.sourceTargetType, this.sourceVariableToken, this.sourceVariableItemId, this.sourceVariableName)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(WiredManager.getGson().toJson(new EditorPayload(this.variableName, this.sourceTargetType, this.sourceVariableToken, this.sourceVariableItemId, this.getResolvedSourceName(room)))); + message.appendInt(0); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + this.sourceTargetType = normalizeTargetType(data.sourceTargetType); + this.sourceVariableToken = normalizeVariableToken(data.sourceVariableToken, data.sourceVariableItemId); + this.sourceVariableItemId = getCustomVariableItemId(this.sourceVariableToken); + this.sourceVariableName = normalizeSourceName(data.sourceVariableName); + } + + @Override + public void onPickUp() { + this.variableName = ""; + this.sourceTargetType = TARGET_USER; + this.sourceVariableToken = ""; + this.sourceVariableItemId = 0; + this.sourceVariableName = ""; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getVariableName() { + return this.variableName; + } + + public int getSourceTargetType() { + return this.sourceTargetType; + } + + public String getSourceVariableToken() { + return this.sourceVariableToken; + } + + public int getSourceVariableItemId() { + return this.sourceVariableItemId; + } + + public String getSourceVariableName() { + return this.sourceVariableName; + } + + public boolean isUserEcho() { + return this.sourceTargetType == TARGET_USER; + } + + public boolean isFurniEcho() { + return this.sourceTargetType == TARGET_FURNI; + } + + public boolean isRoomEcho() { + return this.sourceTargetType == TARGET_ROOM; + } + + public WiredVariableDefinitionInfo createDefinitionInfo(Room room) { + SourceState sourceState = this.resolveSourceState(room, this.sourceTargetType, this.sourceVariableToken, this.sourceVariableItemId); + int availability = (sourceState != null) ? sourceState.getAvailability() : defaultAvailability(this.sourceTargetType); + boolean hasValue = (sourceState == null) || sourceState.hasValue(); + boolean readOnly = sourceState == null || sourceState.isReadOnly(); + + return new WiredVariableDefinitionInfo( + this.getId(), + this.variableName, + hasValue, + availability, + WiredVariableTextConnectorSupport.isTextConnected(room, this), + readOnly + ); + } + + public boolean hasVariable(Room room, int entityId) { + if (room == null) { + return false; + } + + if (isCustomVariableToken(this.sourceVariableToken)) { + return switch (this.sourceTargetType) { + case TARGET_FURNI -> room.getFurniVariableManager().hasVariable(entityId, this.sourceVariableItemId); + case TARGET_ROOM -> room.getRoomVariableManager().hasVariable(this.sourceVariableItemId); + default -> room.getUserVariableManager().hasVariable(entityId, this.sourceVariableItemId); + }; + } + + return this.readCurrentValue(room, entityId) != null; + } + + public int getCurrentValue(Room room, int entityId) { + Integer value = this.readCurrentValue(room, entityId); + return (value != null) ? value : 0; + } + + public int getCreatedAt(Room room, int entityId) { + if (room == null || !isCustomVariableToken(this.sourceVariableToken)) { + return 0; + } + + return switch (this.sourceTargetType) { + case TARGET_FURNI -> room.getFurniVariableManager().getCreatedAt(entityId, this.sourceVariableItemId); + case TARGET_ROOM -> room.getRoomVariableManager().getCreatedAt(this.sourceVariableItemId); + default -> room.getUserVariableManager().getCreatedAt(entityId, this.sourceVariableItemId); + }; + } + + public int getUpdatedAt(Room room, int entityId) { + if (room == null || !isCustomVariableToken(this.sourceVariableToken)) { + return 0; + } + + return switch (this.sourceTargetType) { + case TARGET_FURNI -> room.getFurniVariableManager().getUpdatedAt(entityId, this.sourceVariableItemId); + case TARGET_ROOM -> room.getRoomVariableManager().getUpdatedAt(this.sourceVariableItemId); + default -> room.getUserVariableManager().getUpdatedAt(entityId, this.sourceVariableItemId); + }; + } + + public boolean assignValue(Room room, int entityId, Integer value, boolean overrideExisting) { + if (room == null) { + return false; + } + + SourceState sourceState = this.resolveSourceState(room, this.sourceTargetType, this.sourceVariableToken, this.sourceVariableItemId); + if (sourceState == null || sourceState.isReadOnly()) { + return false; + } + + if (isCustomVariableToken(this.sourceVariableToken)) { + return switch (this.sourceTargetType) { + case TARGET_FURNI -> room.getFurniVariableManager().assignVariable(room.getHabboItem(entityId), this.sourceVariableItemId, value, overrideExisting); + case TARGET_ROOM -> room.getRoomVariableManager().updateVariableValue(this.sourceVariableItemId, (value != null) ? value : 0); + default -> room.getUserVariableManager().assignVariable(room.getHabbo(entityId), this.sourceVariableItemId, value, overrideExisting); + }; + } + + return value != null && this.writeCurrentValue(room, entityId, value); + } + + public boolean updateValue(Room room, int entityId, Integer value) { + if (room == null) { + return false; + } + + SourceState sourceState = this.resolveSourceState(room, this.sourceTargetType, this.sourceVariableToken, this.sourceVariableItemId); + if (sourceState == null || sourceState.isReadOnly() || !sourceState.hasValue()) { + return false; + } + + if (isCustomVariableToken(this.sourceVariableToken)) { + return switch (this.sourceTargetType) { + case TARGET_FURNI -> room.getFurniVariableManager().updateVariableValue(entityId, this.sourceVariableItemId, value); + case TARGET_ROOM -> room.getRoomVariableManager().updateVariableValue(this.sourceVariableItemId, (value != null) ? value : 0); + default -> room.getUserVariableManager().updateVariableValue(entityId, this.sourceVariableItemId, value); + }; + } + + return value != null && this.writeCurrentValue(room, entityId, value); + } + + public boolean removeValue(Room room, int entityId) { + if (room == null || !isCustomVariableToken(this.sourceVariableToken)) { + return false; + } + + SourceState sourceState = this.resolveSourceState(room, this.sourceTargetType, this.sourceVariableToken, this.sourceVariableItemId); + if (sourceState == null || sourceState.isReadOnly()) { + return false; + } + + return switch (this.sourceTargetType) { + case TARGET_FURNI -> room.getFurniVariableManager().removeVariable(entityId, this.sourceVariableItemId); + case TARGET_ROOM -> room.getRoomVariableManager().removeVariable(this.sourceVariableItemId); + default -> room.getUserVariableManager().removeVariable(entityId, this.sourceVariableItemId); + }; + } + + private Integer readCurrentValue(Room room, int entityId) { + if (room == null || this.sourceVariableToken == null || this.sourceVariableToken.isEmpty()) { + return null; + } + + if (isCustomVariableToken(this.sourceVariableToken)) { + return switch (this.sourceTargetType) { + case TARGET_FURNI -> room.getFurniVariableManager().hasVariable(entityId, this.sourceVariableItemId) + ? room.getFurniVariableManager().getCurrentValue(entityId, this.sourceVariableItemId) + : null; + case TARGET_ROOM -> room.getRoomVariableManager().getCurrentValue(this.sourceVariableItemId); + default -> room.getUserVariableManager().hasVariable(entityId, this.sourceVariableItemId) + ? room.getUserVariableManager().getCurrentValue(entityId, this.sourceVariableItemId) + : null; + }; + } + + String key = getInternalVariableKey(this.sourceVariableToken); + if (key.isEmpty()) { + return null; + } + + return switch (this.sourceTargetType) { + case TARGET_FURNI -> this.readFurniInternalValue(room, room.getHabboItem(entityId), key); + case TARGET_ROOM -> this.readRoomInternalValue(room, key); + default -> { + Habbo habbo = room.getHabbo(entityId); + yield this.readUserInternalValue(room, (habbo != null) ? habbo.getRoomUnit() : null, key); + } + }; + } + + private boolean writeCurrentValue(Room room, int entityId, int value) { + if (room == null || !isInternalVariableToken(this.sourceVariableToken)) { + return false; + } + + String key = getInternalVariableKey(this.sourceVariableToken); + if (key.isEmpty()) { + return false; + } + + return switch (this.sourceTargetType) { + case TARGET_FURNI -> this.writeFurniInternalValue(room, room.getHabboItem(entityId), key, value); + case TARGET_ROOM -> false; + default -> { + Habbo habbo = room.getHabbo(entityId); + yield this.writeUserInternalValue(room, (habbo != null) ? habbo.getRoomUnit() : null, key, value); + } + }; + } + + private SourceState resolveSourceState(Room room, int targetType, String token, int variableItemId) { + if (room == null || token == null || token.isEmpty()) { + return null; + } + + if (isCustomVariableToken(token)) { + WiredVariableDefinitionInfo definitionInfo = switch (targetType) { + case TARGET_FURNI -> room.getFurniVariableManager().getDefinitionInfo(variableItemId); + case TARGET_ROOM -> room.getRoomVariableManager().getDefinitionInfo(variableItemId); + default -> room.getUserVariableManager().getDefinitionInfo(variableItemId); + }; + + if (definitionInfo == null) { + return null; + } + + return new SourceState(definitionInfo.getName(), definitionInfo.hasValue(), definitionInfo.getAvailability(), definitionInfo.isReadOnly()); + } + + String key = getInternalVariableKey(token); + if (key.isEmpty()) { + return null; + } + + return switch (targetType) { + case TARGET_FURNI -> canUseFurniInternalReference(key) + ? new SourceState(key, true, DEFAULT_FURNI_AVAILABILITY, !canUseFurniInternalDestination(key)) + : null; + case TARGET_ROOM -> canUseRoomInternalReference(key) + ? new SourceState(key, true, DEFAULT_ROOM_AVAILABILITY, true) + : null; + default -> canUseUserInternalReference(key) + ? new SourceState(key, true, DEFAULT_USER_AVAILABILITY, !canUseUserInternalDestination(key)) + : null; + }; + } + + private String getResolvedSourceName(Room room) { + SourceState sourceState = this.resolveSourceState(room, this.sourceTargetType, this.sourceVariableToken, this.sourceVariableItemId); + return (sourceState != null) ? sourceState.getName() : this.sourceVariableName; + } + + private static boolean createsCycle(Room room, int currentItemId, int targetType, String token, int variableItemId) { + if (room == null || currentItemId <= 0 || !isCustomVariableToken(token) || variableItemId <= 0) { + return false; + } + + if (variableItemId == currentItemId) { + return true; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(room, targetType, variableItemId); + if (derivedDefinition != null) { + return createsCycle(room, currentItemId, targetType, createCustomVariableToken(derivedDefinition.getBaseDefinitionItemId()), derivedDefinition.getBaseDefinitionItemId()); + } + + if (room.getRoomSpecialTypes() == null) { + return false; + } + + InteractionWiredExtra extra = room.getRoomSpecialTypes().getExtra(variableItemId); + if (!(extra instanceof WiredExtraVariableEcho)) { + return false; + } + + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) extra; + return createsCycle(room, currentItemId, echo.getSourceTargetType(), echo.getSourceVariableToken(), echo.getSourceVariableItemId()); + } + + private static String deriveVariableName(String requestedName, String sourceName) { + String normalizedRequestedName = WiredVariableNameValidator.normalizeForSave(requestedName); + if (!normalizedRequestedName.isEmpty()) { + return normalizedRequestedName; + } + + String fallbackValue = normalizeSourceName(sourceName) + .replaceAll("^[~@]+", "") + .replaceAll("[^A-Za-z0-9_]+", "_") + .replaceAll("_+", "_") + .replaceAll("^_+", "") + .replaceAll("_+$", ""); + + if (fallbackValue.length() > WiredVariableNameValidator.MAX_NAME_LENGTH) { + fallbackValue = fallbackValue.substring(0, WiredVariableNameValidator.MAX_NAME_LENGTH); + } + + return fallbackValue; + } + + private static boolean isAllowedEchoSource(SourceState sourceState, String token) { + if (sourceState == null || token == null || token.isEmpty()) { + return false; + } + + if (isInternalVariableToken(token)) { + return true; + } + + return isCustomVariableToken(token) && sourceState.getName() != null && sourceState.getName().contains("."); + } + + private static ConfigData parseConfigData(String value) { + if (value == null || value.isEmpty() || !value.startsWith("{")) { + return new ConfigData(); + } + + ConfigData config = WiredManager.getGson().fromJson(value, ConfigData.class); + return (config != null) ? config : new ConfigData(); + } + + private static String normalizeSourceName(String value) { + if (value == null) { + return ""; + } + + return value.trim().replace("\t", "").replace("\r", "").replace("\n", ""); + } + + private static int normalizeTargetType(int value) { + if (value == TARGET_FURNI || value == TARGET_ROOM) { + return value; + } + + return TARGET_USER; + } + + private static int defaultAvailability(int targetType) { + return switch (targetType) { + case TARGET_FURNI -> DEFAULT_FURNI_AVAILABILITY; + case TARGET_ROOM -> DEFAULT_ROOM_AVAILABILITY; + default -> DEFAULT_USER_AVAILABILITY; + }; + } + + private static boolean isCustomVariableToken(String token) { + return token != null && token.startsWith(CUSTOM_TOKEN_PREFIX); + } + + private static boolean isInternalVariableToken(String token) { + return token != null && token.startsWith(INTERNAL_TOKEN_PREFIX); + } + + private static String getInternalVariableKey(String token) { + return isInternalVariableToken(token) ? WiredInternalVariableSupport.normalizeKey(token.substring(INTERNAL_TOKEN_PREFIX.length())) : ""; + } + + private static int getCustomVariableItemId(String token) { + if (!isCustomVariableToken(token)) { + return 0; + } + + try { + return Integer.parseInt(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } catch (NumberFormatException ignored) { + return 0; + } + } + + private static String createCustomVariableToken(int itemId) { + return itemId > 0 ? CUSTOM_TOKEN_PREFIX + itemId : ""; + } + + private static String normalizeVariableToken(String token, int fallbackItemId) { + String normalizedToken = (token != null) ? token.trim() : ""; + + if (isCustomVariableToken(normalizedToken)) { + return normalizedToken; + } + + if (isInternalVariableToken(normalizedToken)) { + return INTERNAL_TOKEN_PREFIX + WiredInternalVariableSupport.normalizeKey(normalizedToken.substring(INTERNAL_TOKEN_PREFIX.length())); + } + + if (fallbackItemId > 0) { + return createCustomVariableToken(fallbackItemId); + } + + if (normalizedToken.isEmpty()) { + return ""; + } + + try { + return createCustomVariableToken(Integer.parseInt(normalizedToken)); + } catch (NumberFormatException ignored) { + return ""; + } + } + + private Integer readUserInternalValue(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.readUserValue(room, roomUnit, key); + } + + private Integer getUserTypeValue(Habbo habbo, Bot bot, Pet pet) { + if (habbo != null) return 1; + if (bot != null) return 2; + if (pet != null) return 3; + + return null; + } + + private Integer getGenderValue(Habbo habbo, Bot bot) { + HabboGender gender = null; + + if (habbo != null && habbo.getHabboInfo() != null) { + gender = habbo.getHabboInfo().getGender(); + } else if (bot != null) { + gender = bot.getGender(); + } + + if (gender == null) { + return null; + } + + return gender == HabboGender.F ? 1 : 2; + } + + private boolean writeUserInternalValue(Room room, RoomUnit roomUnit, String key, int value) { + return WiredInternalVariableSupport.writeUserValue(room, roomUnit, key, value); + } + + private Integer readFurniInternalValue(Room room, HabboItem item, String key) { + return WiredInternalVariableSupport.readFurniValue(room, item, key); + } + + private boolean writeFurniInternalValue(Room room, HabboItem item, String key, int value) { + return WiredInternalVariableSupport.writeFurniValue(room, item, key, value); + } + + private Integer readRoomInternalValue(Room room, String key) { + return WiredInternalVariableSupport.readRoomValue(room, key); + } + + private Integer getUserTeamScore(Room room, Habbo habbo) { + if (room == null || habbo == null || habbo.getHabboInfo() == null || habbo.getHabboInfo().getGamePlayer() == null) return null; + + Game game = this.resolveTeamGame(room, habbo); + GamePlayer gamePlayer = habbo.getHabboInfo().getGamePlayer(); + + if (game == null || gamePlayer.getTeamColor() == null) return gamePlayer.getScore(); + + GameTeam team = game.getTeam(gamePlayer.getTeamColor()); + return (team != null) ? team.getTotalScore() : gamePlayer.getScore(); + } + + private int getTeamMetric(Room room, GameTeamColors color, boolean score) { + Game game = this.resolveTeamGame(room, null); + if (game == null || color == null) return 0; + + GameTeam team = game.getTeam(color); + if (team == null) return 0; + + return score ? team.getTotalScore() : team.getMembers().size(); + } + + private int getTeamColorId(int effectId) { + if (effectId >= 33 && effectId <= 36) return effectId - 32; + if (effectId >= 40 && effectId <= 43) return effectId - 39; + return 0; + } + + private int getTeamTypeId(int effectId) { + if (effectId >= 33 && effectId <= 36) return 1; + if (effectId >= 40 && effectId <= 43) return 2; + return 0; + } + + private Game resolveTeamGame(Room room, Habbo habbo) { + if (room == null) return null; + + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getCurrentGame() != null) { + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (game != null) return game; + } + + Game game = room.getGame(com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame.class); + if (game != null) return game; + + game = room.getGame(com.eu.habbo.habbohotel.games.freeze.FreezeGame.class); + if (game != null) return game; + + return room.getGame(com.eu.habbo.habbohotel.games.wired.WiredGame.class); + } + + private boolean moveUserTo(Room room, RoomUnit roomUnit, int x, int y) { + if (room == null || roomUnit == null || room.getLayout() == null) return false; + + RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); + if (targetTile == null || targetTile.state == RoomTileState.INVALID) return false; + + double targetZ = WiredUserMovementHelper.resolveUserTargetZ(room, targetTile); + return WiredUserMovementHelper.moveUser( + room, + roomUnit, + targetTile, + targetZ, + roomUnit.getBodyRotation(), + roomUnit.getHeadRotation(), + WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION, + false); + } + + private boolean moveFurniTo(Room room, HabboItem item, int x, int y, int rotation, double z) { + if (room == null || item == null || room.getLayout() == null) return false; + + RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); + if (targetTile == null || targetTile.state == RoomTileState.INVALID) return false; + + FurnitureMovementError error = room.moveFurniTo(item, targetTile, rotation, z, null, true, true); + return error == FurnitureMovementError.NONE; + } + + private static Integer parseInteger(String value) { + if (value == null || value.isEmpty()) return 0; + + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException ignored) { + return 0; + } + } + + private static boolean canUseUserInternalDestination(String key) { + return WiredInternalVariableSupport.canUseUserDestination(key); + } + + private static boolean canUseFurniInternalDestination(String key) { + return WiredInternalVariableSupport.canUseFurniDestination(key); + } + + private static boolean canUseUserInternalReference(String key) { + return WiredInternalVariableSupport.canUseUserReference(key); + } + + private static boolean canUseFurniInternalReference(String key) { + return WiredInternalVariableSupport.canUseFurniReference(key); + } + + private static boolean canUseRoomInternalReference(String key) { + return WiredInternalVariableSupport.canUseRoomReference(key); + } + + private Integer getRoomEntryMethodValue(Habbo habbo) { + if (habbo == null || habbo.getHabboInfo() == null) { + return null; + } + + String roomEntryMethod = habbo.getHabboInfo().getRoomEntryMethod(); + + if (roomEntryMethod == null || roomEntryMethod.trim().isEmpty()) { + return 0; + } + + return switch (roomEntryMethod.trim().toLowerCase(Locale.ROOT)) { + case "door" -> 1; + case "teleport" -> 2; + default -> 3; + }; + } + + static class JsonData { + String variableName; + int sourceTargetType; + String sourceVariableToken; + int sourceVariableItemId; + String sourceVariableName; + + JsonData(String variableName, int sourceTargetType, String sourceVariableToken, int sourceVariableItemId, String sourceVariableName) { + this.variableName = variableName; + this.sourceTargetType = sourceTargetType; + this.sourceVariableToken = sourceVariableToken; + this.sourceVariableItemId = sourceVariableItemId; + this.sourceVariableName = sourceVariableName; + } + } + + static class ConfigData { + String variableName = ""; + int sourceTargetType = TARGET_USER; + String sourceVariableToken = ""; + int sourceVariableItemId = 0; + } + + static class EditorPayload extends ConfigData { + String sourceVariableName; + + EditorPayload(String variableName, int sourceTargetType, String sourceVariableToken, int sourceVariableItemId, String sourceVariableName) { + this.variableName = variableName; + this.sourceTargetType = sourceTargetType; + this.sourceVariableToken = sourceVariableToken; + this.sourceVariableItemId = sourceVariableItemId; + this.sourceVariableName = sourceVariableName; + } + } + + private static class SourceState { + private final String name; + private final boolean hasValue; + private final int availability; + private final boolean readOnly; + + private SourceState(String name, boolean hasValue, int availability, boolean readOnly) { + this.name = name; + this.hasValue = hasValue; + this.availability = availability; + this.readOnly = readOnly; + } + + public String getName() { + return this.name; + } + + public boolean hasValue() { + return this.hasValue; + } + + public int getAvailability() { + return this.availability; + } + + public boolean isReadOnly() { + return this.readOnly; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableFilterBase.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableFilterBase.java new file mode 100644 index 00000000..a11da8db --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableFilterBase.java @@ -0,0 +1,757 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame; +import com.eu.habbo.habbohotel.games.freeze.FreezeGame; +import com.eu.habbo.habbohotel.games.wired.WiredGame; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.DanceType; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; +import com.eu.habbo.habbohotel.wired.core.WiredInternalVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import com.eu.habbo.util.HotelDateTimeUtil; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +public abstract class WiredExtraVariableFilterBase extends InteractionWiredExtra { + protected static final int TARGET_USER = 0; + protected static final int TARGET_FURNI = 1; + protected static final int TARGET_CONTEXT = 2; + protected static final int TARGET_ROOM = 3; + + protected static final int AMOUNT_CONSTANT = 0; + protected static final int AMOUNT_VARIABLE = 1; + protected static final int SOURCE_SECONDARY_SELECTED = 101; + + protected static final int SORT_VALUE_HIGHEST = 0; + protected static final int SORT_VALUE_LOWEST = 1; + protected static final int SORT_CREATION_OLDEST = 2; + protected static final int SORT_CREATION_LATEST = 3; + protected static final int SORT_UPDATE_OLDEST = 4; + protected static final int SORT_UPDATE_LATEST = 5; + + private static final int MAX_FILTER_AMOUNT = 10000; + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + private static final String INTERNAL_TOKEN_PREFIX = "internal:"; + private static final String DELIM = "\t"; + + protected int sortBy = SORT_VALUE_HIGHEST; + protected int amountMode = AMOUNT_CONSTANT; + protected int amountConstantValue = 1; + protected int referenceTargetType = TARGET_USER; + protected int referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + protected String variableToken = ""; + protected int variableItemId = 0; + protected String referenceVariableToken = ""; + protected int referenceVariableItemId = 0; + protected final List referenceSelectedItems = new ArrayList<>(); + + protected WiredExtraVariableFilterBase(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + protected WiredExtraVariableFilterBase(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + protected abstract int getVariableTargetType(); + + protected abstract int getCode(); + + public List filterUsers(Room room, WiredContext ctx, Iterable values) { + if (room == null || ctx == null || this.getVariableTargetType() != TARGET_USER || this.variableToken.isEmpty()) { + return toUserList(values); + } + + int amount = this.resolveAmount(ctx, room); + if (amount <= 0) return new ArrayList<>(); + + List> matches = new ArrayList<>(); + + for (RoomUnit roomUnit : values) { + if (roomUnit == null) continue; + + MetricSnapshot metric = this.resolveUserMetric(room, roomUnit); + if (metric == null) continue; + + matches.add(new SortableEntry<>(roomUnit, metric)); + } + + matches.sort(this.metricComparator()); + return trimUsers(matches, amount); + } + + public List filterItems(Room room, WiredContext ctx, Iterable values) { + if (room == null || ctx == null || this.getVariableTargetType() != TARGET_FURNI || this.variableToken.isEmpty()) { + return toItemList(values); + } + + int amount = this.resolveAmount(ctx, room); + if (amount <= 0) return new ArrayList<>(); + + List> matches = new ArrayList<>(); + + for (HabboItem item : values) { + if (item == null) continue; + + MetricSnapshot metric = this.resolveFurniMetric(room, item); + if (metric == null) continue; + + matches.add(new SortableEntry<>(item, metric)); + } + + matches.sort(this.metricComparator()); + return trimItems(matches, amount); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) throw new WiredSaveException("Room not found"); + + int[] params = settings.getIntParams(); + String[] stringParts = parseStringData(settings.getStringParam()); + String nextVariableToken = normalizeVariableToken((stringParts.length > 0) ? stringParts[0] : ""); + String nextReferenceVariableToken = normalizeVariableToken((stringParts.length > 1) ? stringParts[1] : ""); + int nextSortBy = normalizeSortBy(param(params, 0, SORT_VALUE_HIGHEST)); + int nextAmountMode = normalizeAmountMode(param(params, 1, AMOUNT_CONSTANT)); + int nextAmountConstantValue = normalizeAmount(param(params, 2, 1)); + int nextReferenceTargetType = normalizeReferenceTargetType(param(params, 3, TARGET_USER)); + int nextReferenceUserSource = normalizeUserSource(param(params, 4, WiredSourceUtil.SOURCE_TRIGGER)); + int nextReferenceFurniSource = normalizeReferenceFurniSource(param(params, 5, WiredSourceUtil.SOURCE_TRIGGER)); + + if (!this.isValidMainVariable(room, nextVariableToken)) throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + if (nextAmountMode == AMOUNT_VARIABLE && !this.isValidReference(room, nextReferenceTargetType, nextReferenceVariableToken)) throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + + List nextReferenceItems = new ArrayList<>(); + if (nextAmountMode == AMOUNT_VARIABLE && nextReferenceTargetType == TARGET_FURNI && nextReferenceFurniSource == SOURCE_SECONDARY_SELECTED) { + int selectionLimit = Emulator.getConfig().getInt("hotel.wired.furni.selection.count"); + if (settings.getFurniIds().length > selectionLimit) throw new WiredSaveException("Too many furni selected"); + + for (int furniId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(furniId); + if (item != null) nextReferenceItems.add(item); + } + } + + this.sortBy = nextSortBy; + this.amountMode = nextAmountMode; + this.amountConstantValue = nextAmountConstantValue; + this.referenceTargetType = nextReferenceTargetType; + this.referenceUserSource = nextReferenceUserSource; + this.referenceFurniSource = nextReferenceFurniSource; + this.setVariableToken(nextVariableToken); + this.setReferenceVariableToken(nextReferenceVariableToken); + this.referenceSelectedItems.clear(); + this.referenceSelectedItems.addAll(nextReferenceItems); + return true; + } + + @Override + public String getWiredData() { + this.refreshReferenceItems(); + return WiredManager.getGson().toJson(new JsonData(this.sortBy, this.amountMode, this.amountConstantValue, this.referenceTargetType, this.referenceUserSource, this.referenceFurniSource, this.variableToken, this.variableItemId, this.referenceVariableToken, this.referenceVariableItemId, this.toIds(this.referenceSelectedItems))); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refreshReferenceItems(); + List selectedItems = new ArrayList<>(); + if (this.amountMode == AMOUNT_VARIABLE && this.referenceTargetType == TARGET_FURNI && this.referenceFurniSource == SOURCE_SECONDARY_SELECTED) { + selectedItems.addAll(this.referenceSelectedItems); + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(selectedItems.size()); + for (HabboItem item : selectedItems) message.appendInt(item.getId()); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.serializeStringData()); + message.appendInt(6); + message.appendInt(this.sortBy); + message.appendInt(this.amountMode); + message.appendInt(this.amountConstantValue); + message.appendInt(this.referenceTargetType); + message.appendInt(this.referenceUserSource); + message.appendInt(this.referenceFurniSource); + message.appendInt(0); + message.appendInt(this.getCode()); + message.appendInt(0); + message.appendInt(0); + } + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) return; + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) return; + + this.sortBy = normalizeSortBy(data.sortBy); + this.amountMode = normalizeAmountMode(data.amountMode); + this.amountConstantValue = normalizeAmount(data.amountConstantValue); + this.referenceTargetType = normalizeReferenceTargetType(data.referenceTargetType); + this.referenceUserSource = normalizeUserSource(data.referenceUserSource); + this.referenceFurniSource = normalizeReferenceFurniSource(data.referenceFurniSource); + this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); + this.setReferenceVariableToken(normalizeVariableToken((data.referenceVariableToken != null) ? data.referenceVariableToken : ((data.referenceVariableItemId > 0) ? String.valueOf(data.referenceVariableItemId) : ""))); + + if (room == null || data.selectedItemIds == null) return; + + for (Integer itemId : data.selectedItemIds) { + if (itemId == null || itemId <= 0) continue; + + HabboItem item = room.getHabboItem(itemId); + if (item != null) this.referenceSelectedItems.add(item); + } + } + + @Override + public void onPickUp() { + this.sortBy = SORT_VALUE_HIGHEST; + this.amountMode = AMOUNT_CONSTANT; + this.amountConstantValue = 1; + this.referenceTargetType = TARGET_USER; + this.referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER; + this.referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.referenceSelectedItems.clear(); + this.setVariableToken(""); + this.setReferenceVariableToken(""); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + private int resolveAmount(WiredContext ctx, Room room) { + if (this.amountMode != AMOUNT_VARIABLE) return normalizeAmount(this.amountConstantValue); + + Integer value = this.resolveReferenceValue(ctx, room); + return value == null ? 0 : normalizeAmount(value); + } + + private Integer resolveReferenceValue(WiredContext ctx, Room room) { + if (room == null) return null; + + if (this.referenceTargetType == TARGET_FURNI) { + int source = (this.referenceFurniSource == SOURCE_SECONDARY_SELECTED) ? WiredSourceUtil.SOURCE_SELECTED : this.referenceFurniSource; + if (source == WiredSourceUtil.SOURCE_SELECTED) this.refreshReferenceItems(); + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, source, this.referenceSelectedItems)) { + Integer value = this.readFurniReferenceValue(room, item); + if (value != null) return value; + } + + return null; + } + + if (this.referenceTargetType == TARGET_CONTEXT) { + return this.readContextReferenceValue(ctx, room); + } + + if (this.referenceTargetType == TARGET_ROOM) { + return this.readRoomReferenceValue(room); + } + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.referenceUserSource)) { + Integer value = this.readUserReferenceValue(room, roomUnit); + if (value != null) return value; + } + + return null; + } + + private Integer readUserReferenceValue(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) return null; + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + return canUseUserInternalReference(key) ? this.readUserInternalValue(room, roomUnit, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + Habbo habbo = room.getHabbo(roomUnit); + return (habbo != null) ? room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.referenceVariableItemId) : null; + } + + private Integer readFurniReferenceValue(Room room, HabboItem item) { + if (room == null || item == null) return null; + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + return canUseFurniInternalReference(key) ? this.readFurniInternalValue(room, item, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.referenceVariableItemId); + return (definition != null && definition.hasValue()) ? room.getFurniVariableManager().getCurrentValue(item.getId(), this.referenceVariableItemId) : null; + } + + private Integer readRoomReferenceValue(Room room) { + if (room == null) return null; + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + return canUseRoomInternalReference(key) ? this.readRoomInternalValue(room, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(this.referenceVariableItemId); + return (definition != null && definition.hasValue()) ? room.getRoomVariableManager().getCurrentValue(this.referenceVariableItemId) : null; + } + + private Integer readContextReferenceValue(WiredContext ctx, Room room) { + if (ctx == null) return null; + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + return canUseContextInternalReference(key) ? WiredInternalVariableSupport.readContextValue(ctx, key) : null; + } + + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, this.referenceVariableItemId); + if (definition == null || !definition.hasValue() || !WiredContextVariableSupport.hasVariable(ctx, this.referenceVariableItemId)) return null; + + return WiredContextVariableSupport.getCurrentValue(ctx, this.referenceVariableItemId); + } + + private MetricSnapshot resolveUserMetric(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + Integer value = canUseUserInternalReference(key) ? this.readUserInternalValue(room, roomUnit, key) : null; + return (value != null) ? new MetricSnapshot(roomUnit.getId(), value, 0, 0) : null; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.variableItemId); + Habbo habbo = room.getHabbo(roomUnit); + if (definition == null || habbo == null) return null; + if (!room.getUserVariableManager().hasVariable(habbo.getHabboInfo().getId(), this.variableItemId)) return null; + + return new MetricSnapshot( + roomUnit.getId(), + definition.hasValue() ? room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.variableItemId) : 0, + room.getUserVariableManager().getCreatedAt(habbo.getHabboInfo().getId(), this.variableItemId), + room.getUserVariableManager().getUpdatedAt(habbo.getHabboInfo().getId(), this.variableItemId)); + } + + private MetricSnapshot resolveFurniMetric(Room room, HabboItem item) { + if (room == null || item == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + Integer value = canUseFurniInternalReference(key) ? this.readFurniInternalValue(room, item, key) : null; + return (value != null) ? new MetricSnapshot(item.getId(), value, 0, 0) : null; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.variableItemId); + if (definition == null) return null; + if (!room.getFurniVariableManager().hasVariable(item.getId(), this.variableItemId)) return null; + + return new MetricSnapshot( + item.getId(), + definition.hasValue() ? room.getFurniVariableManager().getCurrentValue(item.getId(), this.variableItemId) : 0, + room.getFurniVariableManager().getCreatedAt(item.getId(), this.variableItemId), + room.getFurniVariableManager().getUpdatedAt(item.getId(), this.variableItemId)); + } + + private Comparator> metricComparator() { + return switch (this.sortBy) { + case SORT_VALUE_LOWEST -> Comparator.comparingInt((SortableEntry entry) -> entry.metric.value).thenComparingInt(entry -> entry.metric.entityId); + case SORT_CREATION_OLDEST -> Comparator.comparingInt((SortableEntry entry) -> entry.metric.createdAt).thenComparingInt(entry -> entry.metric.entityId); + case SORT_CREATION_LATEST -> Comparator., Integer>comparing(entry -> entry.metric.createdAt).reversed().thenComparingInt(entry -> entry.metric.entityId); + case SORT_UPDATE_OLDEST -> Comparator.comparingInt((SortableEntry entry) -> entry.metric.updatedAt).thenComparingInt(entry -> entry.metric.entityId); + case SORT_UPDATE_LATEST -> Comparator., Integer>comparing(entry -> entry.metric.updatedAt).reversed().thenComparingInt(entry -> entry.metric.entityId); + default -> Comparator., Integer>comparing(entry -> entry.metric.value).reversed().thenComparingInt(entry -> entry.metric.entityId); + }; + } + + private boolean isValidMainVariable(Room room, String token) { + if (token == null || token.isEmpty()) return false; + + if (isInternalVariableToken(token)) { + String key = getInternalVariableKey(token); + return this.getVariableTargetType() == TARGET_FURNI ? canUseFurniInternalReference(key) : canUseUserInternalReference(key); + } + + if (this.getVariableTargetType() == TARGET_FURNI) { + return room != null && room.getFurniVariableManager().getDefinitionInfo(getCustomItemId(token)) != null; + } + + return room != null && room.getUserVariableManager().getDefinitionInfo(getCustomItemId(token)) != null; + } + + private boolean isValidReference(Room room, int targetType, String token) { + if (token == null || token.isEmpty()) return false; + + if (isInternalVariableToken(token)) { + String key = getInternalVariableKey(token); + return switch (targetType) { + case TARGET_FURNI -> canUseFurniInternalReference(key); + case TARGET_CONTEXT -> canUseContextInternalReference(key); + case TARGET_ROOM -> canUseRoomInternalReference(key); + default -> canUseUserInternalReference(key); + }; + } + + return switch (targetType) { + case TARGET_FURNI -> { + WiredVariableDefinitionInfo definition = (room != null) ? room.getFurniVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + yield definition != null && definition.hasValue(); + } + case TARGET_CONTEXT -> this.isValidContextCustomReference(room, getCustomItemId(token)); + case TARGET_ROOM -> { + WiredVariableDefinitionInfo definition = (room != null) ? room.getRoomVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + yield definition != null && definition.hasValue(); + } + default -> { + WiredVariableDefinitionInfo definition = (room != null) ? room.getUserVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + yield definition != null && definition.hasValue(); + } + }; + } + + private boolean isValidContextCustomReference(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, variableItemId); + return definition != null && definition.hasValue(); + } + private static List trimUsers(List> matches, int amount) { + List result = new ArrayList<>(); + for (SortableEntry match : matches) { + if (result.size() >= amount) break; + result.add(match.entity); + } + return result; + } + + private static List trimItems(List> matches, int amount) { + List result = new ArrayList<>(); + for (SortableEntry match : matches) { + if (result.size() >= amount) break; + result.add(match.entity); + } + return result; + } + + private static List toUserList(Iterable values) { + List result = new ArrayList<>(); + if (values == null) return result; + for (RoomUnit value : values) if (value != null) result.add(value); + return result; + } + + private static List toItemList(Iterable values) { + List result = new ArrayList<>(); + if (values == null) return result; + for (HabboItem value : values) if (value != null) result.add(value); + return result; + } + + private String serializeStringData() { + return (this.variableToken == null ? "" : this.variableToken) + DELIM + (this.referenceVariableToken == null ? "" : this.referenceVariableToken); + } + + private void refreshReferenceItems() { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + this.referenceSelectedItems.clear(); + return; + } + + this.referenceSelectedItems.removeIf(item -> item == null || item.getRoomId() != room.getId() || room.getHabboItem(item.getId()) == null); + } + + private void setVariableToken(String token) { + this.variableToken = normalizeVariableToken(token); + this.variableItemId = getCustomItemId(this.variableToken); + } + + private void setReferenceVariableToken(String token) { + this.referenceVariableToken = normalizeVariableToken(token); + this.referenceVariableItemId = getCustomItemId(this.referenceVariableToken); + } + + private List toIds(List items) { + List ids = new ArrayList<>(); + for (HabboItem item : items) if (item != null) ids.add(item.getId()); + return ids; + } + + private Integer readUserInternalValue(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.readUserValue(room, roomUnit, key); + } + + private Integer readFurniInternalValue(Room room, HabboItem item, String key) { + return WiredInternalVariableSupport.readFurniValue(room, item, key); + } + + private Integer readRoomInternalValue(Room room, String key) { + return WiredInternalVariableSupport.readRoomValue(room, key); + } + private int getUserTeamScore(Room room, Habbo habbo) { + if (room == null || habbo == null || habbo.getHabboInfo() == null || habbo.getHabboInfo().getGamePlayer() == null) return 0; + + Game game = this.resolveTeamGame(room, habbo); + if (game == null) return 0; + + GamePlayer player = habbo.getHabboInfo().getGamePlayer(); + return player.getScore(); + } + + private int getTeamColorId(int effectValue) { + TeamEffectData effectData = this.getTeamEffectData(effectValue); + return (effectData != null) ? effectData.colorId : 0; + } + + private int getTeamTypeId(int effectValue) { + TeamEffectData effectData = this.getTeamEffectData(effectValue); + return (effectData != null) ? effectData.typeId : 0; + } + + private int getTeamMetric(Room room, GameTeamColors color, boolean score) { + Game game = room.getGame(WiredGame.class); + if (game == null) game = room.getGame(FreezeGame.class); + if (game == null) game = room.getGame(BattleBanzaiGame.class); + if (game == null) return 0; + + GameTeam team = game.getTeam(color); + if (team == null) return 0; + + return score ? team.getTotalScore() : team.getMembers().size(); + } + + private Game resolveTeamGame(Room room, Habbo habbo) { + if (room == null) return null; + + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getCurrentGame() != null) { + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (game != null) return game; + } + + Game wiredGame = room.getGame(WiredGame.class); + if (wiredGame != null) return wiredGame; + + Game freezeGame = room.getGame(FreezeGame.class); + if (freezeGame != null) return freezeGame; + + return room.getGame(BattleBanzaiGame.class); + } + + private TeamEffectData getTeamEffectData(int effectValue) { + if (effectValue <= 0) return null; + + if (effectValue >= 223 && effectValue <= 226) return new TeamEffectData(effectValue - 222, 0); + if (effectValue >= 33 && effectValue <= 36) return new TeamEffectData(effectValue - 32, 1); + if (effectValue >= 40 && effectValue <= 43) return new TeamEffectData(effectValue - 39, 2); + + return null; + } + + private static int normalizeSortBy(int value) { + return switch (value) { + case SORT_VALUE_LOWEST, SORT_CREATION_OLDEST, SORT_CREATION_LATEST, SORT_UPDATE_OLDEST, SORT_UPDATE_LATEST -> value; + default -> SORT_VALUE_HIGHEST; + }; + } + + private static int normalizeAmountMode(int value) { + return (value == AMOUNT_VARIABLE) ? AMOUNT_VARIABLE : AMOUNT_CONSTANT; + } + + private static int normalizeReferenceTargetType(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + private static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + private static int normalizeReferenceFurniSource(int value) { + return switch (value) { + case SOURCE_SECONDARY_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + } + + protected static String normalizeVariableToken(String token) { + if (token == null) return ""; + + String normalized = token.trim(); + if (normalized.isEmpty()) return ""; + if (normalized.startsWith(INTERNAL_TOKEN_PREFIX)) { + return INTERNAL_TOKEN_PREFIX + WiredInternalVariableSupport.normalizeKey(normalized.substring(INTERNAL_TOKEN_PREFIX.length())); + } + if (isCustomVariableToken(normalized) || isInternalVariableToken(normalized)) return normalized; + + try { + int parsed = Integer.parseInt(normalized); + return (parsed > 0) ? (CUSTOM_TOKEN_PREFIX + parsed) : ""; + } catch (NumberFormatException e) { + return ""; + } + } + + protected static boolean isCustomVariableToken(String token) { + return token != null && token.startsWith(CUSTOM_TOKEN_PREFIX); + } + + protected static boolean isInternalVariableToken(String token) { + return token != null && token.startsWith(INTERNAL_TOKEN_PREFIX); + } + + protected static int getCustomItemId(String token) { + if (!isCustomVariableToken(token)) return 0; + + try { + return Integer.parseInt(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } catch (NumberFormatException e) { + return 0; + } + } + + protected static String getInternalVariableKey(String token) { + return isInternalVariableToken(token) ? WiredInternalVariableSupport.normalizeKey(token.substring(INTERNAL_TOKEN_PREFIX.length())) : ""; + } + + private static boolean canUseUserInternalReference(String key) { + return WiredInternalVariableSupport.canUseUserReference(key); + } + + private static boolean canUseFurniInternalReference(String key) { + return WiredInternalVariableSupport.canUseFurniReference(key); + } + + private static boolean canUseRoomInternalReference(String key) { + return WiredInternalVariableSupport.canUseRoomReference(key); + } + + private static boolean canUseContextInternalReference(String key) { + return WiredInternalVariableSupport.canUseContextReference(key); + } + + private static int param(int[] params, int index, int fallback) { + return (params != null && params.length > index) ? params[index] : fallback; + } + + private static String[] parseStringData(String value) { + return (value == null || value.isEmpty()) ? new String[0] : value.split("\\t", -1); + } + + private static int parseInteger(String value) { + try { + return (value == null || value.trim().isEmpty()) ? 0 : Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return 0; + } + } + + private static int normalizeAmount(int value) { + return Math.max(0, Math.min(MAX_FILTER_AMOUNT, value)); + } + + protected static class JsonData { + int sortBy; + int amountMode; + int amountConstantValue; + int referenceTargetType; + int referenceUserSource; + int referenceFurniSource; + String variableToken; + int variableItemId; + String referenceVariableToken; + int referenceVariableItemId; + List selectedItemIds; + + JsonData(int sortBy, int amountMode, int amountConstantValue, int referenceTargetType, int referenceUserSource, int referenceFurniSource, String variableToken, int variableItemId, String referenceVariableToken, int referenceVariableItemId, List selectedItemIds) { + this.sortBy = sortBy; + this.amountMode = amountMode; + this.amountConstantValue = amountConstantValue; + this.referenceTargetType = referenceTargetType; + this.referenceUserSource = referenceUserSource; + this.referenceFurniSource = referenceFurniSource; + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.referenceVariableToken = referenceVariableToken; + this.referenceVariableItemId = referenceVariableItemId; + this.selectedItemIds = selectedItemIds; + } + } + + private static class SortableEntry { + final T entity; + final MetricSnapshot metric; + + SortableEntry(T entity, MetricSnapshot metric) { + this.entity = entity; + this.metric = metric; + } + } + + private static class MetricSnapshot { + final int entityId; + final int value; + final int createdAt; + final int updatedAt; + + MetricSnapshot(int entityId, int value, int createdAt, int updatedAt) { + this.entityId = entityId; + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + } + + private static class TeamEffectData { + final int colorId; + final int typeId; + + TeamEffectData(int colorId, int typeId) { + this.colorId = colorId; + this.typeId = typeId; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableLevelUpSystem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableLevelUpSystem.java new file mode 100644 index 00000000..12afd167 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableLevelUpSystem.java @@ -0,0 +1,270 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredExtraVariableLevelUpSystem extends InteractionWiredExtra { + public static final int CODE = 82; + + public static final int MODE_LINEAR = 1; + public static final int MODE_EXPONENTIAL = 2; + public static final int MODE_MANUAL = 3; + + public static final int SUB_CURRENT_LEVEL = 0; + public static final int SUB_CURRENT_XP = 1; + public static final int SUB_LEVEL_PROGRESS = 2; + public static final int SUB_LEVEL_PROGRESS_PERCENT = 3; + public static final int SUB_TOTAL_XP_REQUIRED = 4; + public static final int SUB_XP_REMAINING = 5; + public static final int SUB_IS_AT_MAX = 6; + public static final int SUB_MAX_LEVEL = 7; + public static final int SUBVARIABLE_COUNT = 8; + + private static final int DEFAULT_STEP_SIZE = 100; + private static final int DEFAULT_MAX_LEVEL = 10; + private static final int DEFAULT_FIRST_LEVEL_XP = 100; + private static final int DEFAULT_INCREASE_FACTOR = 100; + private static final int MAX_MANUAL_TEXT_LENGTH = 4096; + + private int mode = MODE_LINEAR; + private int stepSize = DEFAULT_STEP_SIZE; + private int maxLevel = DEFAULT_MAX_LEVEL; + private int firstLevelXp = DEFAULT_FIRST_LEVEL_XP; + private int increaseFactor = DEFAULT_INCREASE_FACTOR; + private String interpolationText = ""; + private int subvariableMask = (1 << SUB_CURRENT_LEVEL) | (1 << SUB_CURRENT_XP); + + public WiredExtraVariableLevelUpSystem(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraVariableLevelUpSystem(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + this.applyConfig(parseJsonData(settings.getStringParam())); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.mode, + this.stepSize, + this.maxLevel, + this.firstLevelXp, + this.increaseFactor, + this.interpolationText, + this.getSelectedSubvariables() + )); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.getWiredData()); + message.appendInt(0); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + this.applyConfig(parseJsonData(wiredData)); + } + + @Override + public void onPickUp() { + this.mode = MODE_LINEAR; + this.stepSize = DEFAULT_STEP_SIZE; + this.maxLevel = DEFAULT_MAX_LEVEL; + this.firstLevelXp = DEFAULT_FIRST_LEVEL_XP; + this.increaseFactor = DEFAULT_INCREASE_FACTOR; + this.interpolationText = ""; + this.subvariableMask = (1 << SUB_CURRENT_LEVEL) | (1 << SUB_CURRENT_XP); + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public int getMode() { + return this.mode; + } + + public int getStepSize() { + return this.stepSize; + } + + public int getMaxLevel() { + return this.maxLevel; + } + + public int getFirstLevelXp() { + return this.firstLevelXp; + } + + public int getIncreaseFactor() { + return this.increaseFactor; + } + + public String getInterpolationText() { + return this.interpolationText; + } + + public boolean hasSubvariable(int subvariableType) { + return subvariableType >= 0 + && subvariableType < SUBVARIABLE_COUNT + && ((this.subvariableMask & (1 << subvariableType)) != 0); + } + + public List getSelectedSubvariables() { + List result = new ArrayList<>(); + + for (int index = 0; index < SUBVARIABLE_COUNT; index++) { + if (this.hasSubvariable(index)) { + result.add(index); + } + } + + return result; + } + + private void applyConfig(JsonData data) { + if (data == null) { + this.onPickUp(); + return; + } + + this.mode = normalizeMode(data.mode); + this.stepSize = normalizeNonNegative(data.stepSize, DEFAULT_STEP_SIZE); + this.maxLevel = normalizeMaxLevel(data.maxLevel); + this.firstLevelXp = normalizeNonNegative(data.firstLevelXp, DEFAULT_FIRST_LEVEL_XP); + this.increaseFactor = normalizeNonNegative(data.increaseFactor, DEFAULT_INCREASE_FACTOR); + this.interpolationText = normalizeInterpolationText(data.interpolationText); + this.subvariableMask = normalizeSubvariableMask(data.subvariables); + } + + private static JsonData parseJsonData(String value) { + if (value == null || value.trim().isEmpty()) { + return new JsonData(); + } + + try { + if (value.trim().startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(value, JsonData.class); + return (data != null) ? data : new JsonData(); + } + } catch (Exception ignored) { + } + + JsonData fallback = new JsonData(); + fallback.interpolationText = normalizeInterpolationText(value); + fallback.mode = MODE_MANUAL; + return fallback; + } + + private static int normalizeMode(int value) { + return switch (value) { + case MODE_EXPONENTIAL, MODE_MANUAL -> value; + default -> MODE_LINEAR; + }; + } + + private static int normalizeNonNegative(int value, int fallback) { + return Math.max(0, (value > 0) ? value : fallback); + } + + private static int normalizeMaxLevel(int value) { + return Math.max(1, (value > 0) ? value : DEFAULT_MAX_LEVEL); + } + + private static String normalizeInterpolationText(String value) { + if (value == null) { + return ""; + } + + String normalized = value.replace("\r", ""); + if (normalized.length() > MAX_MANUAL_TEXT_LENGTH) { + normalized = normalized.substring(0, MAX_MANUAL_TEXT_LENGTH); + } + + return normalized; + } + + private static int normalizeSubvariableMask(List subvariables) { + if (subvariables == null) { + return (1 << SUB_CURRENT_LEVEL) | (1 << SUB_CURRENT_XP); + } + + int mask = 0; + for (Integer subvariable : subvariables) { + if (subvariable == null || subvariable < 0 || subvariable >= SUBVARIABLE_COUNT) { + continue; + } + + mask |= (1 << subvariable); + } + + return mask; + } + + static class JsonData { + int mode = MODE_LINEAR; + int stepSize = DEFAULT_STEP_SIZE; + int maxLevel = DEFAULT_MAX_LEVEL; + int firstLevelXp = DEFAULT_FIRST_LEVEL_XP; + int increaseFactor = DEFAULT_INCREASE_FACTOR; + String interpolationText = ""; + List subvariables = null; + + JsonData() { + } + + JsonData(int mode, int stepSize, int maxLevel, int firstLevelXp, int increaseFactor, String interpolationText, List subvariables) { + this.mode = mode; + this.stepSize = stepSize; + this.maxLevel = maxLevel; + this.firstLevelXp = firstLevelXp; + this.increaseFactor = increaseFactor; + this.interpolationText = interpolationText; + this.subvariables = subvariables; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableReference.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableReference.java new file mode 100644 index 00000000..96970464 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableReference.java @@ -0,0 +1,321 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredExtraVariableReference extends InteractionWiredExtra { + public static final int CODE = 81; + + private String variableName = ""; + private int sourceRoomId = 0; + private String sourceRoomName = ""; + private int sourceVariableItemId = 0; + private String sourceVariableName = ""; + private int sourceTargetType = WiredVariableReferenceSupport.TARGET_USER; + private boolean hasValue = false; + private boolean readOnly = true; + + public WiredExtraVariableReference(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraVariableReference(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + throw new WiredSaveException("Room not found"); + } + + ConfigData config = parseConfigData(settings.getStringParam()); + String normalizedName = WiredVariableNameValidator.normalizeForSave(config.variableName); + + WiredVariableNameValidator.validateDefinitionName(room, this.getId(), normalizedName); + + if (config.sourceRoomId <= 0 || config.sourceVariableItemId <= 0) { + throw new WiredSaveException("wiredfurni.params.variables.validation.missing_variable"); + } + + WiredVariableReferenceSupport.SharedDefinitionOption definition = WiredVariableReferenceSupport.findSharedDefinition( + room, + config.sourceRoomId, + config.sourceVariableItemId, + config.sourceTargetType + ); + + if (definition == null) { + throw new WiredSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + this.variableName = normalizedName; + this.sourceRoomId = definition.getRoomId(); + this.sourceRoomName = sanitizeLabel(definition.getRoomName()); + this.sourceVariableItemId = definition.getItemId(); + this.sourceVariableName = definition.getName(); + this.sourceTargetType = definition.getTargetType(); + this.hasValue = definition.hasValue(); + this.readOnly = config.readOnly; + + room.getUserVariableManager().broadcastSnapshot(); + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.variableName, + this.sourceRoomId, + this.sourceRoomName, + this.sourceVariableItemId, + this.sourceVariableName, + this.sourceTargetType, + this.hasValue, + this.readOnly + )); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(buildEditorPayload(room)); + message.appendInt(0); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + this.sourceRoomId = Math.max(0, data.sourceRoomId); + this.sourceRoomName = sanitizeLabel(data.sourceRoomName); + this.sourceVariableItemId = Math.max(0, data.sourceVariableItemId); + this.sourceVariableName = WiredVariableNameValidator.normalizeLegacy(data.sourceVariableName); + this.sourceTargetType = normalizeTargetType(data.sourceTargetType); + this.hasValue = data.hasValue; + this.readOnly = data.readOnly; + } + + @Override + public void onPickUp() { + this.variableName = ""; + this.sourceRoomId = 0; + this.sourceRoomName = ""; + this.sourceVariableItemId = 0; + this.sourceVariableName = ""; + this.sourceTargetType = WiredVariableReferenceSupport.TARGET_USER; + this.hasValue = false; + this.readOnly = true; + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getVariableName() { + return this.variableName; + } + + public int getSourceRoomId() { + return this.sourceRoomId; + } + + public String getSourceRoomName() { + return this.sourceRoomName; + } + + public int getSourceVariableItemId() { + return this.sourceVariableItemId; + } + + public String getSourceVariableName() { + return this.sourceVariableName; + } + + public int getSourceTargetType() { + return this.sourceTargetType; + } + + public boolean hasValue() { + return this.hasValue; + } + + public boolean isReadOnly() { + return this.readOnly; + } + + public int getAvailability() { + return WiredVariableReferenceSupport.SHARED_AVAILABILITY; + } + + public boolean isUserReference() { + return this.sourceTargetType == WiredVariableReferenceSupport.TARGET_USER; + } + + public boolean isRoomReference() { + return this.sourceTargetType == WiredVariableReferenceSupport.TARGET_ROOM; + } + + private String buildEditorPayload(Room room) { + List roomOptions = new ArrayList<>(); + + for (WiredVariableReferenceSupport.RoomOption option : WiredVariableReferenceSupport.loadRoomOptions(room)) { + List variables = new ArrayList<>(); + + for (WiredVariableReferenceSupport.SharedDefinitionOption definition : option.getVariables()) { + variables.add(new VariableEditorData(definition.getItemId(), definition.getName(), definition.getTargetType(), definition.hasValue())); + } + + roomOptions.add(new RoomEditorData(option.getRoomId(), option.getRoomName(), variables)); + } + + return WiredManager.getGson().toJson(new EditorPayload( + this.variableName, + this.sourceRoomId, + this.sourceRoomName, + this.sourceVariableItemId, + this.sourceVariableName, + this.sourceTargetType, + this.readOnly, + roomOptions + )); + } + + private static ConfigData parseConfigData(String value) { + if (value == null || value.isEmpty() || !value.startsWith("{")) { + return new ConfigData(); + } + + ConfigData config = WiredManager.getGson().fromJson(value, ConfigData.class); + return (config != null) ? config : new ConfigData(); + } + + private static int normalizeTargetType(int value) { + return (value == WiredVariableReferenceSupport.TARGET_ROOM) ? WiredVariableReferenceSupport.TARGET_ROOM : WiredVariableReferenceSupport.TARGET_USER; + } + + private static String sanitizeLabel(String value) { + if (value == null) { + return ""; + } + + return value.trim().replace("\t", "").replace("\r", "").replace("\n", ""); + } + + static class JsonData { + String variableName; + int sourceRoomId; + String sourceRoomName; + int sourceVariableItemId; + String sourceVariableName; + int sourceTargetType; + boolean hasValue; + boolean readOnly; + + JsonData(String variableName, int sourceRoomId, String sourceRoomName, int sourceVariableItemId, String sourceVariableName, int sourceTargetType, boolean hasValue, boolean readOnly) { + this.variableName = variableName; + this.sourceRoomId = sourceRoomId; + this.sourceRoomName = sourceRoomName; + this.sourceVariableItemId = sourceVariableItemId; + this.sourceVariableName = sourceVariableName; + this.sourceTargetType = sourceTargetType; + this.hasValue = hasValue; + this.readOnly = readOnly; + } + } + + static class ConfigData { + String variableName = ""; + int sourceRoomId = 0; + int sourceVariableItemId = 0; + int sourceTargetType = WiredVariableReferenceSupport.TARGET_USER; + boolean readOnly = true; + } + + static class EditorPayload extends ConfigData { + String sourceRoomName; + String sourceVariableName; + List rooms; + + EditorPayload(String variableName, int sourceRoomId, String sourceRoomName, int sourceVariableItemId, String sourceVariableName, int sourceTargetType, boolean readOnly, List rooms) { + this.variableName = variableName; + this.sourceRoomId = sourceRoomId; + this.sourceRoomName = sourceRoomName; + this.sourceVariableItemId = sourceVariableItemId; + this.sourceVariableName = sourceVariableName; + this.sourceTargetType = sourceTargetType; + this.readOnly = readOnly; + this.rooms = rooms; + } + } + + static class RoomEditorData { + int roomId; + String roomName; + List variables; + + RoomEditorData(int roomId, String roomName, List variables) { + this.roomId = roomId; + this.roomName = roomName; + this.variables = variables; + } + } + + static class VariableEditorData { + int itemId; + String name; + int targetType; + boolean hasValue; + + VariableEditorData(int itemId, String name, int targetType, boolean hasValue) { + this.itemId = itemId; + this.name = name; + this.targetType = targetType; + this.hasValue = hasValue; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java new file mode 100644 index 00000000..96149a5d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredExtraVariableTextConnector.java @@ -0,0 +1,231 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +public class WiredExtraVariableTextConnector extends InteractionWiredExtra { + public static final int CODE = 79; + public static final int MAX_MAPPING_LENGTH = 1000; + public static final int MAX_MAPPING_LINES = 30; + + private String mappingsText = ""; + private LinkedHashMap mappings = new LinkedHashMap<>(); + + public WiredExtraVariableTextConnector(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredExtraVariableTextConnector(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + String mappingsText = normalizeMappingsText(settings.getStringParam()); + validateMappingsText(mappingsText); + this.setMappingsText(mappingsText); + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room != null) { + WiredContextVariableSupport.broadcastDefinitions(room); + } + + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.mappingsText)); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.mappingsText); + message.appendInt(0); + message.appendInt(0); + message.appendInt(CODE); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data != null) { + this.setMappingsText(data.mappingsText); + } + + return; + } + + this.setMappingsText(wiredData); + } + + @Override + public void onPickUp() { + this.mappingsText = ""; + this.mappings = new LinkedHashMap<>(); + } + + @Override + public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) { + } + + @Override + public boolean hasConfiguration() { + return true; + } + + public String getMappingsText() { + return this.mappingsText; + } + + public Map getMappings() { + return Collections.unmodifiableMap(this.mappings); + } + + public String resolveText(Integer value) { + if (value == null) { + return ""; + } + + String mappedValue = this.mappings.get(value); + return mappedValue != null ? mappedValue : String.valueOf(value); + } + + public Integer resolveValue(String text) { + if (text == null) { + return null; + } + + String normalizedText = text.trim(); + if (normalizedText.isEmpty()) { + return null; + } + + for (Map.Entry entry : this.mappings.entrySet()) { + if (entry == null || entry.getKey() == null || entry.getValue() == null) { + continue; + } + + if (entry.getValue().trim().equalsIgnoreCase(normalizedText)) { + return entry.getKey(); + } + } + + return null; + } + + private void setMappingsText(String value) { + this.mappingsText = normalizeMappingsText(value); + this.mappings = parseMappings(this.mappingsText); + } + + private static String normalizeMappingsText(String value) { + if (value == null) { + return ""; + } + + return value.replace("\r", ""); + } + + private static void validateMappingsText(String value) throws WiredSaveException { + if (value == null || value.isEmpty()) { + return; + } + + if (value.length() > MAX_MAPPING_LENGTH) { + throw new WiredSaveException("Variable text connector can contain at most 1000 characters."); + } + + int lineCount = 1; + for (int i = 0; i < value.length(); i++) { + if (value.charAt(i) == '\n') { + lineCount++; + } + } + + if (lineCount > MAX_MAPPING_LINES) { + throw new WiredSaveException("Variable text connector can contain at most 30 lines."); + } + } + + private static LinkedHashMap parseMappings(String value) { + LinkedHashMap result = new LinkedHashMap<>(); + if (value == null || value.isEmpty()) { + return result; + } + + for (String rawLine : value.split("\n")) { + if (rawLine == null) { + continue; + } + + String line = rawLine.trim(); + if (line.isEmpty()) { + continue; + } + + int separatorIndex = line.indexOf('='); + if (separatorIndex < 0) { + separatorIndex = line.indexOf(','); + } + + if (separatorIndex <= 0) { + continue; + } + + String keyPart = line.substring(0, separatorIndex).trim(); + String valuePart = line.substring(separatorIndex + 1).trim(); + + try { + result.put(Integer.parseInt(keyPart), valuePart); + } catch (NumberFormatException ignored) { + } + } + + return result; + } + + static class JsonData { + String mappingsText; + + JsonData(String mappingsText) { + this.mappingsText = mappingsText; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableNameValidator.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableNameValidator.java new file mode 100644 index 00000000..5b3ed4ad --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableNameValidator.java @@ -0,0 +1,111 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.util.regex.Pattern; + +final class WiredVariableNameValidator { + static final int MIN_NAME_LENGTH = 1; + static final int MAX_NAME_LENGTH = 40; + + private static final Pattern VALID_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$"); + + private WiredVariableNameValidator() { + } + + static String normalizeForSave(String value) { + if (value == null) { + return ""; + } + + return value + .replace("\t", "") + .replace("\r", "") + .replace("\n", "") + .replaceAll("\\s+", "_"); + } + + static String normalizeLegacy(String value) { + String normalized = normalizeForSave(value); + + if (normalized.contains("=")) { + normalized = normalized.substring(0, normalized.indexOf('=')); + } + + while (normalized.startsWith("@") || normalized.startsWith("~")) { + normalized = normalized.substring(1); + } + + if (normalized.length() > MAX_NAME_LENGTH) { + normalized = normalized.substring(0, MAX_NAME_LENGTH); + } + + return normalized; + } + + static void validateDefinitionName(Room room, int currentItemId, String variableName) throws WiredSaveException { + String normalized = normalizeForSave(variableName); + + if (normalized.length() < MIN_NAME_LENGTH || normalized.length() > MAX_NAME_LENGTH) { + throw new WiredSaveException("wiredfurni.error.variables.name_length"); + } + + if (!VALID_NAME_PATTERN.matcher(normalized).matches()) { + throw new WiredSaveException("wiredfurni.error.variables.name_syntax"); + } + + if (isNameInUse(room, currentItemId, normalized)) { + throw new WiredSaveException("wiredfurni.error.variables.name_uniq"); + } + } + + private static boolean isNameInUse(Room room, int currentItemId, String variableName) { + if (room == null || room.getRoomSpecialTypes() == null || variableName == null || variableName.isEmpty()) { + return false; + } + + for (InteractionWiredExtra extra : room.getRoomSpecialTypes().getExtras()) { + if (extra == null || extra.getId() == currentItemId) { + continue; + } + + String existingName = getDefinitionName(extra); + + if (existingName != null && existingName.equalsIgnoreCase(variableName)) { + return true; + } + } + + return false; + } + + private static String getDefinitionName(InteractionWiredExtra extra) { + if (extra instanceof WiredExtraUserVariable) { + return ((WiredExtraUserVariable) extra).getVariableName(); + } + + if (extra instanceof WiredExtraFurniVariable) { + return ((WiredExtraFurniVariable) extra).getVariableName(); + } + + if (extra instanceof WiredExtraRoomVariable) { + return ((WiredExtraRoomVariable) extra).getVariableName(); + } + + if (extra instanceof WiredExtraContextVariable) { + return ((WiredExtraContextVariable) extra).getVariableName(); + } + + if (extra instanceof WiredExtraVariableReference) { + return ((WiredExtraVariableReference) extra).getVariableName(); + } + + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getVariableName(); + } + + return null; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java new file mode 100644 index 00000000..c83819b2 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java @@ -0,0 +1,630 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.extra; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public final class WiredVariableReferenceSupport { + public static final int TARGET_USER = 0; + public static final int TARGET_ROOM = 3; + public static final int SHARED_AVAILABILITY = 11; + + private static final Logger LOGGER = LoggerFactory.getLogger(WiredVariableReferenceSupport.class); + + private static final ConcurrentHashMap USER_ASSIGNMENT_CACHE = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap ROOM_ASSIGNMENT_CACHE = new ConcurrentHashMap<>(); + + private WiredVariableReferenceSupport() { + } + + public static boolean isSharedAvailability(int availability) { + return availability == SHARED_AVAILABILITY; + } + + public static SharedDefinitionOption findSharedDefinition(Room room, int sourceRoomId, int sourceVariableItemId, int sourceTargetType) { + if (room == null || sourceRoomId <= 0 || sourceVariableItemId <= 0) { + return null; + } + + for (RoomOption roomOption : loadRoomOptions(room)) { + if (roomOption.getRoomId() != sourceRoomId) { + continue; + } + + for (SharedDefinitionOption definition : roomOption.getVariables()) { + if (definition.getItemId() == sourceVariableItemId && definition.getTargetType() == sourceTargetType) { + return definition; + } + } + } + + return null; + } + + public static List loadRoomOptions(Room room) { + if (room == null || room.getOwnerId() <= 0) { + return Collections.emptyList(); + } + + Map optionsByRoomId = new LinkedHashMap<>(); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT rooms.id AS room_id, rooms.name AS room_name, items.id AS item_id, items.wired_data, items_base.interaction_type " + + "FROM rooms " + + "INNER JOIN items ON rooms.id = items.room_id " + + "INNER JOIN items_base ON items.item_id = items_base.id " + + "WHERE rooms.owner_id = ? AND rooms.id <> ? AND items_base.interaction_type IN ('wf_var_user', 'wf_var_room') " + + "ORDER BY rooms.name ASC, items.id ASC")) { + statement.setInt(1, room.getOwnerId()); + statement.setInt(2, room.getId()); + + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + SharedDefinitionOption definition = parseSharedDefinition( + set.getString("interaction_type"), + set.getInt("item_id"), + set.getString("wired_data"), + set.getInt("room_id"), + set.getString("room_name") + ); + + if (definition == null) { + continue; + } + + RoomOption roomOption = optionsByRoomId.computeIfAbsent( + definition.getRoomId(), + key -> new RoomOption(definition.getRoomId(), definition.getRoomName(), new ArrayList<>()) + ); + + roomOption.getVariables().add(definition); + } + } + } catch (SQLException e) { + LOGGER.error("Failed to load shared variable reference options for room {}", room.getId(), e); + } + + List result = new ArrayList<>(optionsByRoomId.values()); + + for (RoomOption option : result) { + option.getVariables().sort(Comparator.comparing(SharedDefinitionOption::getName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(SharedDefinitionOption::getItemId)); + } + + result.sort(Comparator.comparing(RoomOption::getRoomName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(RoomOption::getRoomId)); + return result; + } + + public static SharedUserAssignment getSharedUserAssignment(WiredExtraVariableReference reference, int userId) { + if (reference == null || !reference.isUserReference() || userId <= 0) { + return null; + } + + String cacheKey = createUserCacheKey(reference.getSourceRoomId(), reference.getSourceVariableItemId(), userId); + CachedUserAssignment cachedValue = USER_ASSIGNMENT_CACHE.get(cacheKey); + + if (cachedValue != null) { + return cachedValue.present ? cachedValue.toAssignment() : null; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT value, created_at, updated_at FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ? LIMIT 1")) { + statement.setInt(1, reference.getSourceRoomId()); + statement.setInt(2, userId); + statement.setInt(3, reference.getSourceVariableItemId()); + + try (ResultSet set = statement.executeQuery()) { + if (!set.next()) { + USER_ASSIGNMENT_CACHE.put(cacheKey, CachedUserAssignment.missing()); + return null; + } + + Integer value = null; + int rawValue = set.getInt("value"); + if (!set.wasNull()) { + value = rawValue; + } + + int createdAt = normalizeTimestamp(set.getInt("created_at"), 0); + SharedUserAssignment assignment = new SharedUserAssignment( + value, + createdAt, + normalizeTimestamp(set.getInt("updated_at"), createdAt) + ); + + USER_ASSIGNMENT_CACHE.put(cacheKey, CachedUserAssignment.present(assignment)); + return assignment; + } + } catch (SQLException e) { + LOGGER.error("Failed to load shared wired user variable {} for room {} user {}", reference.getSourceVariableItemId(), reference.getSourceRoomId(), userId, e); + return null; + } + } + + public static boolean assignSharedUserVariable(WiredExtraVariableReference reference, int userId, Integer value, boolean overrideExisting) { + if (reference == null || !reference.isUserReference() || reference.isReadOnly() || userId <= 0 || !isSharedSourceStillAvailable(reference)) { + return false; + } + + Integer normalizedValue = reference.hasValue() ? value : null; + SharedUserAssignment existingAssignment = getSharedUserAssignment(reference, userId); + + if (existingAssignment != null && !overrideExisting) { + return false; + } + + int now = Emulator.getIntUnixTimestamp(); + boolean overwritten = existingAssignment != null && overrideExisting; + SharedUserAssignment nextAssignment = (existingAssignment == null || overwritten) + ? new SharedUserAssignment(normalizedValue, now, now) + : new SharedUserAssignment(normalizedValue, existingAssignment.getCreatedAt(), Objects.equals(existingAssignment.getValue(), normalizedValue) ? existingAssignment.getUpdatedAt() : now); + + if (!overwritten && existingAssignment != null && Objects.equals(existingAssignment.getValue(), normalizedValue)) { + return false; + } + + upsertSharedUserAssignment(reference.getSourceRoomId(), reference.getSourceVariableItemId(), userId, nextAssignment); + USER_ASSIGNMENT_CACHE.put(createUserCacheKey(reference.getSourceRoomId(), reference.getSourceVariableItemId(), userId), CachedUserAssignment.present(nextAssignment)); + return true; + } + + public static boolean updateSharedUserVariable(WiredExtraVariableReference reference, int userId, Integer value) { + if (reference == null || !reference.isUserReference() || reference.isReadOnly() || userId <= 0 || !reference.hasValue() || !isSharedSourceStillAvailable(reference)) { + return false; + } + + SharedUserAssignment existingAssignment = getSharedUserAssignment(reference, userId); + if (existingAssignment == null || Objects.equals(existingAssignment.getValue(), value)) { + return false; + } + + SharedUserAssignment nextAssignment = new SharedUserAssignment(value, existingAssignment.getCreatedAt(), Emulator.getIntUnixTimestamp()); + upsertSharedUserAssignment(reference.getSourceRoomId(), reference.getSourceVariableItemId(), userId, nextAssignment); + USER_ASSIGNMENT_CACHE.put(createUserCacheKey(reference.getSourceRoomId(), reference.getSourceVariableItemId(), userId), CachedUserAssignment.present(nextAssignment)); + return true; + } + + public static boolean removeSharedUserVariable(WiredExtraVariableReference reference, int userId) { + if (reference == null || !reference.isUserReference() || reference.isReadOnly() || userId <= 0 || !isSharedSourceStillAvailable(reference)) { + return false; + } + + SharedUserAssignment existingAssignment = getSharedUserAssignment(reference, userId); + if (existingAssignment == null) { + return false; + } + + deleteSharedUserAssignment(reference.getSourceRoomId(), reference.getSourceVariableItemId(), userId); + USER_ASSIGNMENT_CACHE.put(createUserCacheKey(reference.getSourceRoomId(), reference.getSourceVariableItemId(), userId), CachedUserAssignment.missing()); + return true; + } + + public static void cacheSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId, Integer value, int createdAt, int updatedAt) { + USER_ASSIGNMENT_CACHE.put(createUserCacheKey(sourceRoomId, sourceVariableItemId, userId), CachedUserAssignment.present(new SharedUserAssignment(value, createdAt, updatedAt))); + } + + public static void clearSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId) { + USER_ASSIGNMENT_CACHE.put(createUserCacheKey(sourceRoomId, sourceVariableItemId, userId), CachedUserAssignment.missing()); + } + + public static void clearSharedUserDefinition(int sourceRoomId, int sourceVariableItemId) { + String prefix = createDefinitionPrefix(sourceRoomId, sourceVariableItemId) + ":"; + USER_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix)); + } + + public static SharedRoomAssignment getSharedRoomAssignment(WiredExtraVariableReference reference) { + if (reference == null || !reference.isRoomReference()) { + return null; + } + + String cacheKey = createRoomCacheKey(reference.getSourceRoomId(), reference.getSourceVariableItemId()); + CachedRoomAssignment cachedValue = ROOM_ASSIGNMENT_CACHE.get(cacheKey); + + if (cachedValue != null) { + return cachedValue.present ? cachedValue.toAssignment() : null; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT value, updated_at FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ? LIMIT 1")) { + statement.setInt(1, reference.getSourceRoomId()); + statement.setInt(2, reference.getSourceVariableItemId()); + + try (ResultSet set = statement.executeQuery()) { + if (!set.next()) { + ROOM_ASSIGNMENT_CACHE.put(cacheKey, CachedRoomAssignment.missing()); + return null; + } + + SharedRoomAssignment assignment = new SharedRoomAssignment(set.getInt("value"), normalizeTimestamp(set.getInt("updated_at"), 0)); + ROOM_ASSIGNMENT_CACHE.put(cacheKey, CachedRoomAssignment.present(assignment)); + return assignment; + } + } catch (SQLException e) { + LOGGER.error("Failed to load shared wired room variable {} for room {}", reference.getSourceVariableItemId(), reference.getSourceRoomId(), e); + return null; + } + } + + public static boolean updateSharedRoomVariable(WiredExtraVariableReference reference, int value) { + if (reference == null || !reference.isRoomReference() || reference.isReadOnly() || !isSharedSourceStillAvailable(reference)) { + return false; + } + + SharedRoomAssignment existingAssignment = getSharedRoomAssignment(reference); + if (existingAssignment != null && existingAssignment.getValue() == value) { + return false; + } + + SharedRoomAssignment nextAssignment = new SharedRoomAssignment(value, Emulator.getIntUnixTimestamp()); + upsertSharedRoomAssignment(reference.getSourceRoomId(), reference.getSourceVariableItemId(), nextAssignment); + ROOM_ASSIGNMENT_CACHE.put(createRoomCacheKey(reference.getSourceRoomId(), reference.getSourceVariableItemId()), CachedRoomAssignment.present(nextAssignment)); + return true; + } + + public static boolean removeSharedRoomVariable(WiredExtraVariableReference reference) { + if (reference == null || !reference.isRoomReference() || reference.isReadOnly() || !isSharedSourceStillAvailable(reference)) { + return false; + } + + SharedRoomAssignment existingAssignment = getSharedRoomAssignment(reference); + if (existingAssignment == null) { + return false; + } + + deleteSharedRoomAssignment(reference.getSourceRoomId(), reference.getSourceVariableItemId()); + ROOM_ASSIGNMENT_CACHE.put(createRoomCacheKey(reference.getSourceRoomId(), reference.getSourceVariableItemId()), CachedRoomAssignment.missing()); + return true; + } + + public static void cacheSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId, int value, int updatedAt) { + ROOM_ASSIGNMENT_CACHE.put(createRoomCacheKey(sourceRoomId, sourceVariableItemId), CachedRoomAssignment.present(new SharedRoomAssignment(value, updatedAt))); + } + + public static void clearSharedRoomDefinition(int sourceRoomId, int sourceVariableItemId) { + ROOM_ASSIGNMENT_CACHE.put(createRoomCacheKey(sourceRoomId, sourceVariableItemId), CachedRoomAssignment.missing()); + } + + private static SharedDefinitionOption parseSharedDefinition(String interactionType, int itemId, String wiredData, int roomId, String roomName) { + if ("wf_var_user".equals(interactionType)) { + UserDefinitionData data = parseUserDefinitionData(wiredData); + if (data == null || !isSharedAvailability(data.availability) || data.variableName.isEmpty()) { + return null; + } + + return new SharedDefinitionOption(roomId, roomName, itemId, data.variableName, TARGET_USER, data.hasValue); + } + + if ("wf_var_room".equals(interactionType)) { + RoomDefinitionData data = parseRoomDefinitionData(wiredData); + if (data == null || !isSharedAvailability(data.availability) || data.variableName.isEmpty()) { + return null; + } + + return new SharedDefinitionOption(roomId, roomName, itemId, data.variableName, TARGET_ROOM, true); + } + + return null; + } + + private static UserDefinitionData parseUserDefinitionData(String wiredData) { + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) { + return null; + } + + UserDefinitionData data = WiredManager.getGson().fromJson(wiredData, UserDefinitionData.class); + if (data == null) { + return null; + } + + data.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + return data; + } + + private static RoomDefinitionData parseRoomDefinitionData(String wiredData) { + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) { + return null; + } + + RoomDefinitionData data = WiredManager.getGson().fromJson(wiredData, RoomDefinitionData.class); + if (data == null) { + return null; + } + + data.variableName = WiredVariableNameValidator.normalizeLegacy(data.variableName); + return data; + } + + private static boolean isSharedSourceStillAvailable(WiredExtraVariableReference reference) { + if (reference == null || reference.getSourceRoomId() <= 0 || reference.getSourceVariableItemId() <= 0) { + return false; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT items.wired_data, items_base.interaction_type " + + "FROM items INNER JOIN items_base ON items.item_id = items_base.id " + + "WHERE items.id = ? AND items.room_id = ? LIMIT 1")) { + statement.setInt(1, reference.getSourceVariableItemId()); + statement.setInt(2, reference.getSourceRoomId()); + + try (ResultSet set = statement.executeQuery()) { + if (!set.next()) { + return false; + } + + SharedDefinitionOption definition = parseSharedDefinition( + set.getString("interaction_type"), + reference.getSourceVariableItemId(), + set.getString("wired_data"), + reference.getSourceRoomId(), + "" + ); + + return definition != null && definition.getTargetType() == reference.getSourceTargetType(); + } + } catch (SQLException e) { + LOGGER.error("Failed to validate shared wired variable source {} in room {}", reference.getSourceVariableItemId(), reference.getSourceRoomId(), e); + return false; + } + } + + private static void upsertSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId, SharedUserAssignment assignment) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO room_user_wired_variables (room_id, user_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) { + statement.setInt(1, sourceRoomId); + statement.setInt(2, userId); + statement.setInt(3, sourceVariableItemId); + + if (assignment.getValue() == null) { + statement.setNull(4, java.sql.Types.INTEGER); + } else { + statement.setInt(4, assignment.getValue()); + } + + statement.setInt(5, assignment.getCreatedAt()); + statement.setInt(6, assignment.getUpdatedAt()); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to store shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e); + } + } + + private static void deleteSharedUserAssignment(int sourceRoomId, int sourceVariableItemId, int userId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ?")) { + statement.setInt(1, sourceRoomId); + statement.setInt(2, userId); + statement.setInt(3, sourceVariableItemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete shared wired user variable {} for room {} user {}", sourceVariableItemId, sourceRoomId, userId, e); + } + } + + private static void upsertSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId, SharedRoomAssignment assignment) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO room_wired_variables (room_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) { + statement.setInt(1, sourceRoomId); + statement.setInt(2, sourceVariableItemId); + statement.setInt(3, assignment.getValue()); + statement.setInt(4, 0); + statement.setInt(5, assignment.getUpdatedAt()); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to store shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e); + } + } + + private static void deleteSharedRoomAssignment(int sourceRoomId, int sourceVariableItemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ?")) { + statement.setInt(1, sourceRoomId); + statement.setInt(2, sourceVariableItemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete shared wired room variable {} for room {}", sourceVariableItemId, sourceRoomId, e); + } + } + + private static String createDefinitionPrefix(int sourceRoomId, int sourceVariableItemId) { + return sourceRoomId + ":" + sourceVariableItemId; + } + + private static String createUserCacheKey(int sourceRoomId, int sourceVariableItemId, int userId) { + return createDefinitionPrefix(sourceRoomId, sourceVariableItemId) + ":" + userId; + } + + private static String createRoomCacheKey(int sourceRoomId, int sourceVariableItemId) { + return createDefinitionPrefix(sourceRoomId, sourceVariableItemId); + } + + private static int normalizeTimestamp(int value, int fallback) { + if (value > 0) { + return value; + } + + if (fallback > 0) { + return fallback; + } + + return Emulator.getIntUnixTimestamp(); + } + + public static class RoomOption { + private final int roomId; + private final String roomName; + private final List variables; + + public RoomOption(int roomId, String roomName, List variables) { + this.roomId = roomId; + this.roomName = roomName; + this.variables = variables; + } + + public int getRoomId() { + return this.roomId; + } + + public String getRoomName() { + return this.roomName; + } + + public List getVariables() { + return this.variables; + } + } + + public static class SharedDefinitionOption { + private final int roomId; + private final String roomName; + private final int itemId; + private final String name; + private final int targetType; + private final boolean hasValue; + + public SharedDefinitionOption(int roomId, String roomName, int itemId, String name, int targetType, boolean hasValue) { + this.roomId = roomId; + this.roomName = roomName; + this.itemId = itemId; + this.name = name; + this.targetType = targetType; + this.hasValue = hasValue; + } + + public int getRoomId() { + return this.roomId; + } + + public String getRoomName() { + return this.roomName; + } + + public int getItemId() { + return this.itemId; + } + + public String getName() { + return this.name; + } + + public int getTargetType() { + return this.targetType; + } + + public boolean hasValue() { + return this.hasValue; + } + } + + public static class SharedUserAssignment { + private final Integer value; + private final int createdAt; + private final int updatedAt; + + public SharedUserAssignment(Integer value, int createdAt, int updatedAt) { + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Integer getValue() { + return this.value; + } + + public int getCreatedAt() { + return this.createdAt; + } + + public int getUpdatedAt() { + return this.updatedAt; + } + } + + public static class SharedRoomAssignment { + private final int value; + private final int updatedAt; + + public SharedRoomAssignment(int value, int updatedAt) { + this.value = value; + this.updatedAt = updatedAt; + } + + public int getValue() { + return this.value; + } + + public int getUpdatedAt() { + return this.updatedAt; + } + } + + private static class CachedUserAssignment { + private final boolean present; + private final SharedUserAssignment assignment; + + private CachedUserAssignment(boolean present, SharedUserAssignment assignment) { + this.present = present; + this.assignment = assignment; + } + + private static CachedUserAssignment present(SharedUserAssignment assignment) { + return new CachedUserAssignment(true, assignment); + } + + private static CachedUserAssignment missing() { + return new CachedUserAssignment(false, null); + } + + private SharedUserAssignment toAssignment() { + return this.assignment; + } + } + + private static class CachedRoomAssignment { + private final boolean present; + private final SharedRoomAssignment assignment; + + private CachedRoomAssignment(boolean present, SharedRoomAssignment assignment) { + this.present = present; + this.assignment = assignment; + } + + private static CachedRoomAssignment present(SharedRoomAssignment assignment) { + return new CachedRoomAssignment(true, assignment); + } + + private static CachedRoomAssignment missing() { + return new CachedRoomAssignment(false, null); + } + + private SharedRoomAssignment toAssignment() { + return this.assignment; + } + } + + private static class UserDefinitionData { + String variableName; + boolean hasValue; + int availability; + } + + private static class RoomDefinitionData { + String variableName; + int availability; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/interfaces/InteractionWiredMatchFurniSettings.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/interfaces/InteractionWiredMatchFurniSettings.java index 6db447f7..25bbb7b4 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/interfaces/InteractionWiredMatchFurniSettings.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/interfaces/InteractionWiredMatchFurniSettings.java @@ -8,4 +8,5 @@ public interface InteractionWiredMatchFurniSettings { public boolean shouldMatchState(); public boolean shouldMatchRotation(); public boolean shouldMatchPosition(); + public boolean shouldMatchAltitude(); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniAltitude.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniAltitude.java new file mode 100644 index 00000000..b77a2c1c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniAltitude.java @@ -0,0 +1,225 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWired; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedHashSet; +import java.util.Set; + +public class WiredEffectFurniAltitude extends InteractionWiredEffect { + private static final int COMPARISON_LESS = 0; + private static final int COMPARISON_EQUAL = 1; + private static final int COMPARISON_GREATER = 2; + + public static final WiredEffectType type = WiredEffectType.FURNI_ALTITUDE_SELECTOR; + + private int comparison = COMPARISON_EQUAL; + private double altitude = 0.0D; + private boolean filterExisting = false; + private boolean invert = false; + + public WiredEffectFurniAltitude(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectFurniAltitude(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) { + return; + } + + boolean includeWiredItems = this.includeWiredTargets(ctx); + + Set matchingItems = new LinkedHashSet<>(); + + room.getFloorItems().forEach(item -> { + if (item == null || (!includeWiredItems && item instanceof InteractionWired)) { + return; + } + + if (this.matchesAltitude(item)) { + matchingItems.add(item); + } + }); + + Set result = new LinkedHashSet<>(matchingItems); + + result = this.applySelectorModifiers(result, this.getSelectableFloorItems(room, ctx), ctx.targets().items(), this.filterExisting, this.invert); + + ctx.targets().setItems(result); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + int[] params = settings.getIntParams(); + if (params == null || params.length < 3) { + throw new WiredSaveException("wf_slc_furni_altitude requires 3 int params: comparison, filterExisting, invert"); + } + + this.comparison = this.normalizeComparison(params[0]); + this.filterExisting = params[1] == 1; + this.invert = params[2] == 1; + this.altitude = this.parseAltitudeOrDefault(settings.getStringParam()); + this.setDelay(settings.getDelay()); + + return true; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public boolean isSelector() { + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.comparison, + this.formatAltitude(this.altitude), + this.filterExisting, + this.invert, + this.getDelay() + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.comparison = this.normalizeComparison(data.comparison); + this.altitude = this.parseAltitudeOrDefault(data.altitude); + this.filterExisting = data.filterExisting; + this.invert = data.invert; + this.setDelay(data.delay); + } + + @Override + public void onPickUp() { + this.comparison = COMPARISON_EQUAL; + this.altitude = 0.0D; + this.filterExisting = false; + this.invert = false; + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.formatAltitude(this.altitude)); + message.appendInt(3); + message.appendInt(this.comparison); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + private boolean matchesAltitude(HabboItem item) { + if (item == null) { + return false; + } + + double normalizedAltitude = this.normalizeAltitude(item.getZ()); + + switch (this.comparison) { + case COMPARISON_LESS: + return normalizedAltitude < this.altitude; + case COMPARISON_GREATER: + return normalizedAltitude > this.altitude; + default: + return BigDecimal.valueOf(normalizedAltitude).compareTo(BigDecimal.valueOf(this.altitude)) == 0; + } + } + + private int normalizeComparison(int value) { + if (value < COMPARISON_LESS || value > COMPARISON_GREATER) { + return COMPARISON_EQUAL; + } + + return value; + } + + private double normalizeAltitude(double value) { + double clampedValue = Math.max(0.0D, Math.min(Room.MAXIMUM_FURNI_HEIGHT, value)); + return BigDecimal.valueOf(clampedValue).setScale(2, RoundingMode.HALF_UP).doubleValue(); + } + + private double parseAltitudeOrDefault(String value) { + if (value == null || value.trim().isEmpty()) { + return 0.0D; + } + + try { + return this.normalizeAltitude(new BigDecimal(value.trim()).doubleValue()); + } catch (NumberFormatException exception) { + return 0.0D; + } + } + + private String formatAltitude(double value) { + BigDecimal decimal = BigDecimal.valueOf(this.normalizeAltitude(value)).stripTrailingZeros(); + return (decimal.scale() < 0 ? decimal.setScale(0, RoundingMode.DOWN) : decimal).toPlainString(); + } + + static class JsonData { + int comparison; + String altitude; + boolean filterExisting; + boolean invert; + int delay; + + JsonData(int comparison, String altitude, boolean filterExisting, boolean invert, int delay) { + this.comparison = comparison; + this.altitude = altitude; + this.filterExisting = filterExisting; + this.invert = invert; + this.delay = delay; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniArea.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniArea.java index 30db5f3d..7abbc94a 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniArea.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniArea.java @@ -28,6 +28,8 @@ public class WiredEffectFurniArea extends InteractionWiredEffect { private int rootY = 0; private int areaWidth = 0; private int areaHeight = 0; + private boolean filterExisting = false; + private boolean invert = false; public WiredEffectFurniArea(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -42,13 +44,16 @@ public class WiredEffectFurniArea extends InteractionWiredEffect { Room room = ctx.room(); if (room == null || areaWidth <= 0 || areaHeight <= 0) return; - List furniInArea = getFurniInArea(room); - if (!furniInArea.isEmpty()) { - ctx.targets().setItems(furniInArea); - } + List furniInArea = getFurniInArea(room, this.includeWiredTargets(ctx)); + ctx.targets().setItems(this.applySelectorModifiers( + furniInArea, + this.getSelectableFloorItems(room, ctx), + ctx.targets().items(), + this.filterExisting, + this.invert)); } - private List getFurniInArea(Room room) { + private List getFurniInArea(Room room, boolean includeWiredItems) { List result = new ArrayList<>(); int maxX = rootX + areaWidth - 1; @@ -57,7 +62,7 @@ public class WiredEffectFurniArea extends InteractionWiredEffect { for (int x = rootX; x <= maxX; x++) { for (int y = rootY; y <= maxY; y++) { for (HabboItem item : room.getItemsAt(x, y)) { - if (item != null && !(item instanceof InteractionWired) && !result.contains(item)) { + if (item != null && (includeWiredItems || !(item instanceof InteractionWired)) && !result.contains(item)) { result.add(item); } } @@ -78,6 +83,8 @@ public class WiredEffectFurniArea extends InteractionWiredEffect { this.rootY = params[1]; this.areaWidth = params[2]; this.areaHeight = params[3]; + this.filterExisting = params.length >= 5 && params[4] == 1; + this.invert = params.length >= 6 && params[5] == 1; this.setDelay(settings.getDelay()); return true; @@ -95,7 +102,7 @@ public class WiredEffectFurniArea extends InteractionWiredEffect { @Override public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, getDelay())); + return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay())); } @Override @@ -108,6 +115,8 @@ public class WiredEffectFurniArea extends InteractionWiredEffect { this.rootY = data.rootY; this.areaWidth = data.width; this.areaHeight = data.height; + this.filterExisting = data.filterExisting; + this.invert = data.invert; this.setDelay(data.delay); } } @@ -118,6 +127,8 @@ public class WiredEffectFurniArea extends InteractionWiredEffect { this.rootY = 0; this.areaWidth = 0; this.areaHeight = 0; + this.filterExisting = false; + this.invert = false; this.setDelay(0); } @@ -131,11 +142,13 @@ public class WiredEffectFurniArea extends InteractionWiredEffect { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(4); + message.appendInt(6); message.appendInt(this.rootX); message.appendInt(this.rootY); message.appendInt(this.areaWidth); message.appendInt(this.areaHeight); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); message.appendInt(0); message.appendInt(this.getType().code); @@ -148,18 +161,38 @@ public class WiredEffectFurniArea extends InteractionWiredEffect { return false; } + public int getRootX() { + return this.rootX; + } + + public int getRootY() { + return this.rootY; + } + + public int getAreaWidth() { + return this.areaWidth; + } + + public int getAreaHeight() { + return this.areaHeight; + } + static class JsonData { int rootX; int rootY; int width; int height; + boolean filterExisting; + boolean invert; int delay; - JsonData(int rootX, int rootY, int width, int height, int delay) { + JsonData(int rootX, int rootY, int width, int height, boolean filterExisting, boolean invert, int delay) { this.rootX = rootX; this.rootY = rootY; this.width = width; this.height = height; + this.filterExisting = filterExisting; + this.invert = invert; this.delay = delay; } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java index 5edfed63..02c2630d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniByType.java @@ -11,6 +11,7 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.wired.WiredSaveException; @@ -48,8 +49,9 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { Room room = ctx.room(); if (room == null) return; + boolean includeWiredItems = this.includeWiredTargets(ctx); + List sourceFurni = resolveSourceFurni(ctx, room); - if (sourceFurni.isEmpty()) return; Set matchKeys = new LinkedHashSet<>(); for (HabboItem src : sourceFurni) { @@ -59,33 +61,19 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { matchKeys.add(key); } - Set result = new LinkedHashSet<>(); + Set matched = new LinkedHashSet<>(); room.getFloorItems().forEach(item -> { - if (item instanceof InteractionWired) return; + if (!includeWiredItems && item instanceof InteractionWired) return; String key = matchState ? item.getBaseItem().getId() + ":" + item.getExtradata() : String.valueOf(item.getBaseItem().getId()); if (matchKeys.contains(key)) { - result.add(item); + matched.add(item); } }); - if (filterExisting) { - result.retainAll(ctx.targets().items()); - } - - if (invert) { - Set all = new LinkedHashSet<>(); - room.getFloorItems().forEach(item -> { - if (!(item instanceof InteractionWired)) all.add(item); - }); - all.removeAll(result); - if (!all.isEmpty()) { - ctx.targets().setItems(all); - } - } else if (!result.isEmpty()) { - ctx.targets().setItems(result); - } + Set result = this.applySelectorModifiers(matched, this.getSelectableFloorItems(room, ctx), ctx.targets().items(), filterExisting, invert); + ctx.targets().setItems(result); } private List resolveSourceFurni(WiredContext ctx, Room room) { @@ -97,12 +85,10 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { .collect(Collectors.toList()); } case SOURCE_FURNI_SIGNAL: { - return new ArrayList<>(ctx.targets().items()); + return WiredSourceUtil.resolveItemsRaw(ctx, WiredSourceUtil.SOURCE_SIGNAL, null); } case SOURCE_FURNI_TRIGGER: { - return ctx.sourceItem() - .map(Collections::singletonList) - .orElse(Collections.emptyList()); + return WiredSourceUtil.resolveItemsRaw(ctx, WiredSourceUtil.SOURCE_TRIGGER, null); } default: return Collections.emptyList(); @@ -112,17 +98,17 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { @Override public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { int[] params = settings.getIntParams(); - if (params == null || params.length < 1) { - throw new WiredSaveException("wf_slc_furni_bytype: intParams must have at least 1 element"); + if (params == null || params.length < 4) { + throw new WiredSaveException("wf_slc_furni_bytype: intParams must have at least 4 elements"); } - this.sourceType = params[0]; + this.sourceType = normalizeSourceType(params[0]); this.matchState = params.length > 1 && params[1] == 1; this.filterExisting = params.length > 2 && params[2] == 1; this.invert = params.length > 3 && params[3] == 1; this.pickedFurniIds = new ArrayList<>(); - if (this.sourceType == SOURCE_FURNI_PICKED && settings.getFurniIds() != null) { + if (settings.getFurniIds() != null) { for (int id : settings.getFurniIds()) { if (pickedFurniIds.size() >= MAX_PICKED_FURNI) break; pickedFurniIds.add(id); @@ -135,12 +121,10 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { @Override public void serializeWiredData(ServerMessage message, Room room) { - boolean pickMode = (sourceType == SOURCE_FURNI_PICKED); + message.appendBoolean(true); + message.appendInt(MAX_PICKED_FURNI); - message.appendBoolean(pickMode); - message.appendInt(pickMode ? MAX_PICKED_FURNI : 0); - - if (pickMode && !pickedFurniIds.isEmpty()) { + if (!pickedFurniIds.isEmpty()) { message.appendInt(pickedFurniIds.size()); pickedFurniIds.forEach(message::appendInt); } else { @@ -152,7 +136,7 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { message.appendString(""); message.appendInt(4); - message.appendInt(sourceType); + message.appendInt(this.sourceType); message.appendInt(matchState ? 1 : 0); message.appendInt(filterExisting ? 1 : 0); message.appendInt(invert ? 1 : 0); @@ -182,7 +166,7 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { String wiredData = set.getString("wired_data"); if (wiredData != null && wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.sourceType = data.sourceType; + this.sourceType = normalizeSourceType(data.sourceType); this.matchState = data.matchState; this.filterExisting = data.filterExisting; this.invert = data.invert; @@ -204,6 +188,17 @@ public class WiredEffectFurniByType extends InteractionWiredEffect { @Override public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { return false; } + private int normalizeSourceType(int value) { + switch (value) { + case SOURCE_FURNI_SIGNAL: + case SOURCE_FURNI_TRIGGER: + case SOURCE_FURNI_PICKED: + return value; + default: + return SOURCE_FURNI_PICKED; + } + } + static class JsonData { int sourceType; boolean matchState; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java index f3439707..8ee8a944 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniNeighborhood.java @@ -42,6 +42,8 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { private int sourceType = SOURCE_USER_TRIGGER; private boolean filterExisting = false; private boolean invert = false; + private int targetOffsetX = 0; + private int targetOffsetY = 0; private List tileOffsets = new ArrayList<>(); private List pickedFurniIds = new ArrayList<>(); @@ -59,6 +61,8 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { Room room = ctx.room(); if (room == null || tileOffsets.isEmpty()) return; + boolean includeWiredItems = this.includeWiredTargets(ctx); + List sourcePositions = resolveSourcePositions(ctx, room); if (sourcePositions.isEmpty()) return; @@ -68,12 +72,12 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { for (int[] src : sourcePositions) { LOGGER.info("[FurniNeighborhood] Source: ({},{}), offsets: {}", src[0], src[1], tileOffsets.size()); for (int[] offset : tileOffsets) { - int tx = src[0] + offset[0]; - int ty = src[1] + offset[1]; + int tx = src[0] + (offset[0] - this.targetOffsetX); + int ty = src[1] + (offset[1] - this.targetOffsetY); for (HabboItem item : room.getItemsAt(tx, ty)) { if (item == null) continue; totalRaw++; - if (item instanceof InteractionWired) { + if (!includeWiredItems && item instanceof InteractionWired) { wiredSkipped++; LOGGER.info("[FurniNeighborhood] SKIP wired item {} ({}) at ({},{})", item.getId(), item.getClass().getSimpleName(), tx, ty); @@ -87,18 +91,7 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { } LOGGER.info("[FurniNeighborhood] Raw={}, wiredSkipped={}, kept={}", totalRaw, wiredSkipped, result.size()); - if (filterExisting) { - result.retainAll(ctx.targets().items()); - } - - if (invert) { - Set all = new LinkedHashSet<>(); - room.getFloorItems().forEach(item -> { - if (!(item instanceof InteractionWired)) all.add(item); - }); - all.removeAll(result); - result = all; - } + result = this.applySelectorModifiers(result, this.getSelectableFloorItems(room, ctx), ctx.targets().items(), filterExisting, invert); // Always set the selector result — even if empty. // An empty result means no items matched the neighborhood, so downstream @@ -108,24 +101,46 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { } private List resolveSourcePositions(WiredContext ctx, Room room) { - - if (isUserGroup(sourceType)) { - // Prefer the event tile for user-based sources because during walk-on/walk-off - // events the user's position (getX/getY) hasn't been updated yet (stale position). - // The event tile correctly represents where the triggering action occurred. - if (ctx.tile().isPresent()) { - return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y }); - } - List positions = ctx.targets().users().stream() - .map(u -> new int[]{ u.getX(), u.getY() }) - .collect(Collectors.toList()); - if (positions.isEmpty()) { - ctx.actor().ifPresent(a -> positions.add(new int[]{ a.getX(), a.getY() })); - } - return positions; - } - switch (sourceType) { + case SOURCE_USER_TRIGGER: { + if (ctx.tile().isPresent()) { + return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y }); + } + + return ctx.actor() + .map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() })) + .orElse(Collections.emptyList()); + } + case SOURCE_USER_SIGNAL: { + List positions = ctx.targets().users().stream() + .map(user -> new int[]{ user.getX(), user.getY() }) + .collect(Collectors.toList()); + + if (!positions.isEmpty()) { + return positions; + } + + return ctx.actor() + .map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() })) + .orElse(Collections.emptyList()); + } + case SOURCE_USER_CLICKED: { + if (ctx.event().getTargetUnit().isPresent()) { + RoomUnit targetUnit = ctx.event().getTargetUnit().get(); + + return Collections.singletonList(new int[]{ targetUnit.getX(), targetUnit.getY() }); + } + + List positions = ctx.targets().users().stream() + .map(user -> new int[]{ user.getX(), user.getY() }) + .collect(Collectors.toList()); + + if (!positions.isEmpty()) { + return positions; + } + + return Collections.emptyList(); + } case SOURCE_FURNI_TRIGGER: { return ctx.sourceItem() .map(i -> Collections.singletonList(new int[]{ i.getX(), i.getY() })) @@ -139,9 +154,17 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { .collect(Collectors.toList()); } case SOURCE_FURNI_SIGNAL: { - return ctx.targets().items().stream() + List positions = ctx.targets().items().stream() .map(i -> new int[]{ i.getX(), i.getY() }) .collect(Collectors.toList()); + + if (!positions.isEmpty()) { + return positions; + } + + return ctx.sourceItem() + .map(item -> Collections.singletonList(new int[]{ item.getX(), item.getY() })) + .orElse(Collections.emptyList()); } default: return Collections.emptyList(); @@ -158,12 +181,14 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { this.sourceType = params[0]; this.filterExisting = params.length > 1 && params[1] == 1; this.invert = params.length > 2 && params[2] == 1; + this.targetOffsetX = params.length > 3 ? params[3] : 0; + this.targetOffsetY = params.length > 4 ? params[4] : 0; this.tileOffsets = new ArrayList<>(); - if (params.length > 3) { - int n = params[3]; + if (params.length > 5) { + int n = params[5]; for (int i = 0; i < n && i < MAX_TILE_OFFSETS; i++) { - int xi = 4 + i * 2; + int xi = 6 + i * 2; if (xi + 1 < params.length) { tileOffsets.add(new int[]{ params[xi], params[xi + 1] }); } @@ -180,8 +205,8 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { this.setDelay(settings.getDelay()); - LOGGER.info("[FurniNeighborhood] saveData: sourceType={}, filterExisting={}, invert={}, offsets={}, pickedFurniIds={}", - sourceType, filterExisting, invert, tileOffsets.size(), pickedFurniIds); + LOGGER.info("[FurniNeighborhood] saveData: sourceType={}, filterExisting={}, invert={}, target=({},{}), offsets={}, pickedFurniIds={}", + sourceType, filterExisting, invert, targetOffsetX, targetOffsetY, tileOffsets.size(), pickedFurniIds); for (int[] o : tileOffsets) { LOGGER.info("[FurniNeighborhood] offset: ({}, {})", o[0], o[1]); } @@ -208,11 +233,13 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { message.appendInt(this.getId()); message.appendString(""); - int paramCount = 4 + tileOffsets.size() * 2; + int paramCount = 6 + tileOffsets.size() * 2; message.appendInt(paramCount); message.appendInt(sourceType); message.appendInt(filterExisting ? 1 : 0); message.appendInt(invert ? 1 : 0); + message.appendInt(targetOffsetX); + message.appendInt(targetOffsetY); message.appendInt(tileOffsets.size()); for (int[] offset : tileOffsets) { message.appendInt(offset[0]); @@ -236,7 +263,7 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { @Override public String getWiredData() { return WiredManager.getGson().toJson( - new JsonData(sourceType, filterExisting, invert, tileOffsets, pickedFurniIds, getDelay())); + new JsonData(sourceType, filterExisting, invert, targetOffsetX, targetOffsetY, tileOffsets, pickedFurniIds, getDelay())); } @Override @@ -247,6 +274,8 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { this.sourceType = data.sourceType; this.filterExisting = data.filterExisting; this.invert = data.invert; + this.targetOffsetX = data.targetOffsetX; + this.targetOffsetY = data.targetOffsetY; this.tileOffsets = data.tileOffsets != null ? data.tileOffsets : new ArrayList<>(); this.pickedFurniIds = data.pickedFurniIds != null ? data.pickedFurniIds : new ArrayList<>(); this.setDelay(data.delay); @@ -258,6 +287,8 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { this.sourceType = SOURCE_USER_TRIGGER; this.filterExisting = false; this.invert = false; + this.targetOffsetX = 0; + this.targetOffsetY = 0; this.tileOffsets = new ArrayList<>(); this.pickedFurniIds = new ArrayList<>(); this.setDelay(0); @@ -270,15 +301,19 @@ public class WiredEffectFurniNeighborhood extends InteractionWiredEffect { int sourceType; boolean filterExisting; boolean invert; + int targetOffsetX; + int targetOffsetY; List tileOffsets; List pickedFurniIds; int delay; - JsonData(int sourceType, boolean filterExisting, boolean invert, + JsonData(int sourceType, boolean filterExisting, boolean invert, int targetOffsetX, int targetOffsetY, List tileOffsets, List pickedFurniIds, int delay) { this.sourceType = sourceType; this.filterExisting = filterExisting; this.invert = invert; + this.targetOffsetX = targetOffsetX; + this.targetOffsetY = targetOffsetY; this.tileOffsets = tileOffsets; this.pickedFurniIds = pickedFurniIds; this.delay = delay; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniOnFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniOnFurni.java new file mode 100644 index 00000000..2b70e9b9 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniOnFurni.java @@ -0,0 +1,340 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWired; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class WiredEffectFurniOnFurni extends InteractionWiredEffect { + private static final double EPSILON = 0.0001D; + + private static final int SELECT_FURNI_ABOVE = 0; + private static final int SELECT_FURNI_BELOW = 1; + private static final int SELECT_FURNI_SAME_HEIGHT = 2; + private static final int SELECT_ALL_FURNI_ON_TILE = 3; + + public static final WiredEffectType type = WiredEffectType.FURNI_ON_FURNI_SELECTOR; + + private final Set items = new LinkedHashSet<>(); + private int selectionType = SELECT_FURNI_ABOVE; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private boolean filterExisting = false; + private boolean invert = false; + + public WiredEffectFurniOnFurni(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectFurniOnFurni(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null || room.getLayout() == null) { + ctx.targets().setItems(Collections.emptySet()); + return; + } + + this.refresh(room); + + List sourceItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + if (sourceItems.isEmpty()) { + ctx.targets().setItems(Collections.emptySet()); + return; + } + + Set result = new LinkedHashSet<>(); + boolean includeWiredItems = this.includeWiredTargets(ctx); + + for (HabboItem sourceItem : sourceItems) { + result.addAll(this.resolveRelatedItems(room, sourceItem, includeWiredItems)); + } + + result = this.applySelectorModifiers(result, this.getSelectableFloorItems(room, ctx), ctx.targets().items(), this.filterExisting, this.invert); + + ctx.targets().setItems(result); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + + this.selectionType = (params.length > 0) ? this.normalizeSelectionType(params[0]) : SELECT_FURNI_ABOVE; + this.furniSource = (params.length > 1) ? this.normalizeFurniSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; + this.filterExisting = params.length > 2 && params[2] == 1; + this.invert = params.length > 3 && params[3] == 1; + + int count = settings.getFurniIds().length; + if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + return false; + } + + if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + + this.items.clear(); + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + return false; + } + + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + this.items.add(item); + } + } + } + + this.setDelay(settings.getDelay()); + return true; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public boolean isSelector() { + return true; + } + + @Override + public String getWiredData() { + this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId())); + + return WiredManager.getGson().toJson(new JsonData( + this.selectionType, + this.furniSource, + this.filterExisting, + this.invert, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), + this.getDelay() + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.selectionType = this.normalizeSelectionType(data.selectionType); + this.furniSource = this.normalizeFurniSource(data.furniSource); + this.filterExisting = data.filterExisting; + this.invert = data.invert; + this.setDelay(data.delay); + + if (room == null || data.itemIds == null) { + return; + } + + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + + if (item != null) { + this.items.add(item); + } + } + } + + @Override + public void onPickUp() { + this.items.clear(); + this.selectionType = SELECT_FURNI_ABOVE; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.filterExisting = false; + this.invert = false; + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(room); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(this.items.size()); + + for (HabboItem item : this.items) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(4); + message.appendInt(this.selectionType); + message.appendInt(this.furniSource); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + private Set resolveRelatedItems(Room room, HabboItem sourceItem, boolean includeWiredItems) { + Set result = new LinkedHashSet<>(); + + if (sourceItem == null) { + return result; + } + + RoomTile baseTile = room.getLayout().getTile(sourceItem.getX(), sourceItem.getY()); + if (baseTile == null) { + return result; + } + + Set occupiedTiles = room.getLayout().getTilesAt(baseTile, sourceItem.getBaseItem().getWidth(), sourceItem.getBaseItem().getLength(), sourceItem.getRotation()); + if (occupiedTiles == null) { + return result; + } + + double sourceBase = this.normalizeAltitude(sourceItem.getZ()); + double sourceTop = this.normalizeAltitude(sourceItem.getZ() + Item.getCurrentHeight(sourceItem)); + + for (RoomTile tile : occupiedTiles) { + if (tile == null) { + continue; + } + + for (HabboItem matchedItem : room.getItemsAt(tile)) { + if (matchedItem == null || (!includeWiredItems && matchedItem instanceof InteractionWired)) { + continue; + } + + if (matchedItem == sourceItem) { + if (this.selectionType == SELECT_FURNI_SAME_HEIGHT || this.selectionType == SELECT_ALL_FURNI_ON_TILE) { + result.add(matchedItem); + } + continue; + } + + if (this.matchesSelectionType(sourceBase, sourceTop, matchedItem)) { + result.add(matchedItem); + } + } + } + + return result; + } + + private boolean matchesSelectionType(double sourceBase, double sourceTop, HabboItem matchedItem) { + double matchedBase = this.normalizeAltitude(matchedItem.getZ()); + double matchedTop = this.normalizeAltitude(matchedItem.getZ() + Item.getCurrentHeight(matchedItem)); + + switch (this.selectionType) { + case SELECT_FURNI_BELOW: + return matchedTop <= (sourceBase + EPSILON); + case SELECT_FURNI_SAME_HEIGHT: + return BigDecimal.valueOf(matchedBase).compareTo(BigDecimal.valueOf(sourceBase)) == 0; + case SELECT_ALL_FURNI_ON_TILE: + return true; + case SELECT_FURNI_ABOVE: + default: + return matchedBase >= (sourceTop - EPSILON); + } + } + + private void refresh(Room room) { + Set invalidItems = new LinkedHashSet<>(); + + if (room == null) { + invalidItems.addAll(this.items); + } else { + for (HabboItem item : this.items) { + if (room.getHabboItem(item.getId()) == null) { + invalidItems.add(item); + } + } + } + + this.items.removeAll(invalidItems); + } + + private int normalizeSelectionType(int value) { + if (value < SELECT_FURNI_ABOVE || value > SELECT_ALL_FURNI_ON_TILE) { + return SELECT_FURNI_ABOVE; + } + + return value; + } + + private int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_TRIGGER: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + private double normalizeAltitude(double value) { + double clampedValue = Math.max(0.0D, Math.min(Room.MAXIMUM_FURNI_HEIGHT, value)); + return BigDecimal.valueOf(clampedValue).setScale(2, RoundingMode.HALF_UP).doubleValue(); + } + + static class JsonData { + int selectionType; + int furniSource; + boolean filterExisting; + boolean invert; + List itemIds; + int delay; + + JsonData(int selectionType, int furniSource, boolean filterExisting, boolean invert, List itemIds, int delay) { + this.selectionType = selectionType; + this.furniSource = furniSource; + this.filterExisting = filterExisting; + this.invert = invert; + this.itemIds = itemIds; + this.delay = delay; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniPicks.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniPicks.java new file mode 100644 index 00000000..26c16426 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniPicks.java @@ -0,0 +1,169 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWired; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class WiredEffectFurniPicks extends InteractionWiredEffect { + private static final int MAX_PICKED_FURNI = 20; + + public static final WiredEffectType type = WiredEffectType.FURNI_PICKS_SELECTOR; + + private boolean filterExisting = false; + private boolean invert = false; + private List pickedFurniIds = new ArrayList<>(); + + public WiredEffectFurniPicks(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectFurniPicks(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) { + return; + } + + boolean includeWiredItems = this.includeWiredTargets(ctx); + + Set result = this.pickedFurniIds.stream() + .map(room::getHabboItem) + .filter(item -> item != null && (includeWiredItems || !(item instanceof InteractionWired))) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + result = this.applySelectorModifiers(result, this.getSelectableFloorItems(room, ctx), ctx.targets().items(), this.filterExisting, this.invert); + + ctx.targets().setItems(result); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + + this.filterExisting = params.length > 0 && params[0] == 1; + this.invert = params.length > 1 && params[1] == 1; + + this.pickedFurniIds = new ArrayList<>(); + if (settings.getFurniIds() != null) { + for (int id : settings.getFurniIds()) { + if (this.pickedFurniIds.size() >= MAX_PICKED_FURNI) break; + this.pickedFurniIds.add(id); + } + } + + this.setDelay(settings.getDelay()); + return true; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public boolean isSelector() { + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.filterExisting, + this.invert, + this.pickedFurniIds, + this.getDelay() + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.filterExisting = data.filterExisting; + this.invert = data.invert; + this.pickedFurniIds = (data.pickedFurniIds != null) ? data.pickedFurniIds : new ArrayList<>(); + this.setDelay(data.delay); + } + + @Override + public void onPickUp() { + this.filterExisting = false; + this.invert = false; + this.pickedFurniIds = new ArrayList<>(); + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(true); + message.appendInt(MAX_PICKED_FURNI); + + if (!this.pickedFurniIds.isEmpty()) { + message.appendInt(this.pickedFurniIds.size()); + this.pickedFurniIds.forEach(message::appendInt); + } else { + message.appendInt(0); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(2); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + static class JsonData { + boolean filterExisting; + boolean invert; + List pickedFurniIds; + int delay; + + JsonData(boolean filterExisting, boolean invert, List pickedFurniIds, int delay) { + this.filterExisting = filterExisting; + this.invert = invert; + this.pickedFurniIds = pickedFurniIds; + this.delay = delay; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniSignal.java new file mode 100644 index 00000000..1ba14216 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniSignal.java @@ -0,0 +1,144 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWired; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +public class WiredEffectFurniSignal extends InteractionWiredEffect { + public static final WiredEffectType type = WiredEffectType.FURNI_SIGNAL_SELECTOR; + + private boolean filterExisting = false; + private boolean invert = false; + + public WiredEffectFurniSignal(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectFurniSignal(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) { + return; + } + + boolean includeWiredItems = this.includeWiredTargets(ctx); + + Set result = new LinkedHashSet<>(); + + if (ctx.eventType() == WiredEvent.Type.SIGNAL_RECEIVED) { + List signalItems = WiredSourceUtil.resolveItems(ctx, WiredSourceUtil.SOURCE_SIGNAL, null); + Set matched = signalItems.stream() + .filter(item -> item != null && (includeWiredItems || !(item instanceof InteractionWired))) + .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); + + result = this.applySelectorModifiers(matched, this.getSelectableFloorItems(room, ctx), ctx.targets().items(), this.filterExisting, this.invert); + } + + ctx.targets().setItems(result); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + this.filterExisting = params.length > 0 && params[0] == 1; + this.invert = params.length > 1 && params[1] == 1; + this.setDelay(settings.getDelay()); + return true; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public boolean isSelector() { + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay())); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.filterExisting = data.filterExisting; + this.invert = data.invert; + this.setDelay(data.delay); + } + + @Override + public void onPickUp() { + this.filterExisting = false; + this.invert = false; + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(2); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + static class JsonData { + boolean filterExisting; + boolean invert; + int delay; + + JsonData(boolean filterExisting, boolean invert, int delay) { + this.filterExisting = filterExisting; + this.invert = invert; + this.delay = delay; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniWithVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniWithVariable.java new file mode 100644 index 00000000..e3d4032c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectFurniWithVariable.java @@ -0,0 +1,29 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.wired.WiredEffectType; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredEffectFurniWithVariable extends WiredEffectVariableSelectorBase { + public static final WiredEffectType type = WiredEffectType.FURNI_WITH_VAR_SELECTOR; + + public WiredEffectFurniWithVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectFurniWithVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + protected int getVariableTargetType() { + return TARGET_FURNI; + } + + @Override + public WiredEffectType getType() { + return type; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersArea.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersArea.java index 46984906..0763cf9e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersArea.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersArea.java @@ -6,7 +6,6 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; -import com.eu.habbo.habbohotel.rooms.RoomUnitType; import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredManager; @@ -29,8 +28,6 @@ public class WiredEffectUsersArea extends InteractionWiredEffect { private int areaHeight = 0; private boolean filterExisting = false; private boolean invert = false; - private boolean excludeBots = false; - private boolean excludePets = false; public WiredEffectUsersArea(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -50,23 +47,15 @@ public class WiredEffectUsersArea extends InteractionWiredEffect { List usersInArea = new ArrayList<>(); for (RoomUnit unit : room.getRoomUnits()) { - if (excludeBots && unit.getRoomUnitType() == RoomUnitType.BOT) continue; - if (excludePets && unit.getRoomUnitType() == RoomUnitType.PET) continue; int x = unit.getX(); int y = unit.getY(); boolean inArea = x >= rootX && x <= maxX && y >= rootY && y <= maxY; - if (invert ? !inArea : inArea) { + if (inArea) { usersInArea.add(unit); } } - if (filterExisting) { - usersInArea.retainAll(ctx.targets().users()); - } - - if (!usersInArea.isEmpty()) { - ctx.targets().setUsers(usersInArea); - } + ctx.targets().setUsers(this.applySelectorModifiers(usersInArea, room.getRoomUnits(), ctx.targets().users(), filterExisting, invert)); } @Override @@ -82,8 +71,6 @@ public class WiredEffectUsersArea extends InteractionWiredEffect { this.areaHeight = params[3]; this.filterExisting = params.length >= 5 && params[4] == 1; this.invert = params.length >= 6 && params[5] == 1; - this.excludeBots = params.length >= 7 && params[6] == 1; - this.excludePets = params.length >= 8 && params[7] == 1; this.setDelay(settings.getDelay()); return true; @@ -101,7 +88,7 @@ public class WiredEffectUsersArea extends InteractionWiredEffect { @Override public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, excludeBots, excludePets, getDelay())); + return WiredManager.getGson().toJson(new JsonData(rootX, rootY, areaWidth, areaHeight, filterExisting, invert, getDelay())); } @Override @@ -116,8 +103,6 @@ public class WiredEffectUsersArea extends InteractionWiredEffect { this.areaHeight = data.height; this.filterExisting = data.filterExisting; this.invert = data.invert; - this.excludeBots = data.excludeBots; - this.excludePets = data.excludePets; this.setDelay(data.delay); } } @@ -130,8 +115,6 @@ public class WiredEffectUsersArea extends InteractionWiredEffect { this.areaHeight = 0; this.filterExisting = false; this.invert = false; - this.excludeBots = false; - this.excludePets = false; this.setDelay(0); } @@ -145,15 +128,13 @@ public class WiredEffectUsersArea extends InteractionWiredEffect { message.appendInt(this.getId()); message.appendString(""); - message.appendInt(8); + message.appendInt(6); message.appendInt(this.rootX); message.appendInt(this.rootY); message.appendInt(this.areaWidth); message.appendInt(this.areaHeight); message.appendInt(this.filterExisting ? 1 : 0); message.appendInt(this.invert ? 1 : 0); - message.appendInt(this.excludeBots ? 1 : 0); - message.appendInt(this.excludePets ? 1 : 0); message.appendInt(0); message.appendInt(this.getType().code); @@ -166,6 +147,22 @@ public class WiredEffectUsersArea extends InteractionWiredEffect { return false; } + public int getRootX() { + return this.rootX; + } + + public int getRootY() { + return this.rootY; + } + + public int getAreaWidth() { + return this.areaWidth; + } + + public int getAreaHeight() { + return this.areaHeight; + } + static class JsonData { int rootX; int rootY; @@ -173,19 +170,15 @@ public class WiredEffectUsersArea extends InteractionWiredEffect { int height; boolean filterExisting; boolean invert; - boolean excludeBots; - boolean excludePets; int delay; - JsonData(int rootX, int rootY, int width, int height, boolean filterExisting, boolean invert, boolean excludeBots, boolean excludePets, int delay) { + JsonData(int rootX, int rootY, int width, int height, boolean filterExisting, boolean invert, int delay) { this.rootX = rootX; this.rootY = rootY; this.width = width; this.height = height; this.filterExisting = filterExisting; this.invert = invert; - this.excludeBots = excludeBots; - this.excludePets = excludePets; this.delay = delay; } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByAction.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByAction.java new file mode 100644 index 00000000..3f88d99a --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByAction.java @@ -0,0 +1,333 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.WiredUserActionType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedHashSet; +import java.util.Set; + +public class WiredEffectUsersByAction extends InteractionWiredEffect { + private static final String CACHE_LAST_ACTION_ID = "wired.last_user_action.id"; + private static final String CACHE_LAST_ACTION_PARAMETER = "wired.last_user_action.parameter"; + private static final String CACHE_LAST_ACTION_TIMESTAMP = "wired.last_user_action.timestamp"; + private static final long TRANSIENT_ACTION_WINDOW_MS = 5_000L; + private static final int DEFAULT_ACTION = WiredUserActionType.WAVE; + + public static final WiredEffectType type = WiredEffectType.USERS_BY_ACTION_SELECTOR; + + private int selectedAction = DEFAULT_ACTION; + private boolean signFilterEnabled = false; + private int signId = 0; + private boolean danceFilterEnabled = false; + private int danceId = 1; + private boolean filterExisting = false; + private boolean invert = false; + + public WiredEffectUsersByAction(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectUsersByAction(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) { + return; + } + + Set result = new LinkedHashSet<>(); + + for (RoomUnit roomUnit : room.getRoomUnits()) { + if (this.matchesAction(ctx, roomUnit)) { + result.add(roomUnit); + } + } + + result = this.applySelectorModifiers(result, room.getRoomUnits(), ctx.targets().users(), this.filterExisting, this.invert); + + ctx.targets().setUsers(result); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + + this.onPickUp(); + + if (params.length > 0) this.selectedAction = this.normalizeAction(params[0]); + if (params.length > 1) this.signFilterEnabled = (params[1] == 1); + if (params.length > 2) this.signId = this.normalizeSignId(params[2]); + if (params.length > 3) this.danceFilterEnabled = (params[3] == 1); + if (params.length > 4) this.danceId = this.normalizeDanceId(params[4]); + if (params.length > 5) this.filterExisting = (params[5] == 1); + if (params.length > 6) this.invert = (params[6] == 1); + + this.setDelay(settings.getDelay()); + return true; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public boolean isSelector() { + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.selectedAction, + this.signFilterEnabled, + this.signId, + this.danceFilterEnabled, + this.danceId, + this.filterExisting, + this.invert, + this.getDelay() + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.selectedAction = this.normalizeAction(data.selectedAction); + this.signFilterEnabled = data.signFilterEnabled; + this.signId = this.normalizeSignId(data.signId); + this.danceFilterEnabled = data.danceFilterEnabled; + this.danceId = this.normalizeDanceId(data.danceId); + this.filterExisting = data.filterExisting; + this.invert = data.invert; + this.setDelay(data.delay); + } + + @Override + public void onPickUp() { + this.selectedAction = DEFAULT_ACTION; + this.signFilterEnabled = false; + this.signId = 0; + this.danceFilterEnabled = false; + this.danceId = 1; + this.filterExisting = false; + this.invert = false; + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(7); + message.appendInt(this.selectedAction); + message.appendInt(this.signFilterEnabled ? 1 : 0); + message.appendInt(this.signId); + message.appendInt(this.danceFilterEnabled ? 1 : 0); + message.appendInt(this.danceId); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + private int normalizeAction(int action) { + switch (action) { + case WiredUserActionType.WAVE: + case WiredUserActionType.BLOW_KISS: + case WiredUserActionType.LAUGH: + case WiredUserActionType.AWAKE: + case WiredUserActionType.RELAX: + case WiredUserActionType.SIT: + case WiredUserActionType.STAND: + case WiredUserActionType.LAY: + case WiredUserActionType.SIGN: + case WiredUserActionType.DANCE: + case WiredUserActionType.THUMB_UP: + return action; + default: + return DEFAULT_ACTION; + } + } + + private int normalizeSignId(int value) { + return (value < 0 || value > 17) ? 0 : value; + } + + private int normalizeDanceId(int value) { + return (value < 1 || value > 4) ? 1 : value; + } + + private boolean matchesAction(WiredContext ctx, RoomUnit roomUnit) { + if (roomUnit == null) { + return false; + } + + if (this.matchesEventAction(ctx, roomUnit)) { + return true; + } + + if (this.matchesCurrentState(roomUnit)) { + return true; + } + + return this.matchesRecentAction(roomUnit); + } + + private boolean matchesEventAction(WiredContext ctx, RoomUnit roomUnit) { + RoomUnit actor = ctx.actor().orElse(null); + + if (actor == null || actor.getId() != roomUnit.getId()) { + return false; + } + + if (ctx.eventType() != WiredEvent.Type.USER_PERFORMS_ACTION) { + return false; + } + + return this.matchesConfiguredAction(ctx.event().getActionId(), ctx.event().getActionParameter()); + } + + private boolean matchesCurrentState(RoomUnit roomUnit) { + switch (this.selectedAction) { + case WiredUserActionType.SIT: + return roomUnit.hasStatus(RoomUnitStatus.SIT); + case WiredUserActionType.LAY: + return roomUnit.hasStatus(RoomUnitStatus.LAY); + case WiredUserActionType.RELAX: + return roomUnit.isIdle(); + case WiredUserActionType.SIGN: + return this.matchesSignState(roomUnit); + case WiredUserActionType.DANCE: + return this.matchesDanceState(roomUnit); + default: + return false; + } + } + + private boolean matchesRecentAction(RoomUnit roomUnit) { + Object actionValue = roomUnit.getCacheable().get(CACHE_LAST_ACTION_ID); + Object parameterValue = roomUnit.getCacheable().get(CACHE_LAST_ACTION_PARAMETER); + Object timestampValue = roomUnit.getCacheable().get(CACHE_LAST_ACTION_TIMESTAMP); + + if (!(actionValue instanceof Integer) || !(timestampValue instanceof Long)) { + return false; + } + + long timestamp = (Long) timestampValue; + if ((System.currentTimeMillis() - timestamp) > TRANSIENT_ACTION_WINDOW_MS) { + return false; + } + + int actionId = (Integer) actionValue; + int parameter = (parameterValue instanceof Integer) ? (Integer) parameterValue : -1; + + return this.matchesConfiguredAction(actionId, parameter); + } + + private boolean matchesConfiguredAction(int actionId, int actionParameter) { + if (actionId != this.selectedAction) { + return false; + } + + if (this.selectedAction == WiredUserActionType.SIGN && this.signFilterEnabled) { + return actionParameter == this.signId; + } + + if (this.selectedAction == WiredUserActionType.DANCE && this.danceFilterEnabled) { + return actionParameter == this.danceId; + } + + return true; + } + + private boolean matchesSignState(RoomUnit roomUnit) { + String signStatus = roomUnit.getStatus(RoomUnitStatus.SIGN); + if (signStatus == null) { + return false; + } + + if (!this.signFilterEnabled) { + return true; + } + + try { + return Integer.parseInt(signStatus) == this.signId; + } catch (NumberFormatException ignored) { + return false; + } + } + + private boolean matchesDanceState(RoomUnit roomUnit) { + int currentDance = roomUnit.getDanceType().getType(); + if (currentDance <= 0) { + return false; + } + + if (!this.danceFilterEnabled) { + return true; + } + + return currentDance == this.danceId; + } + + static class JsonData { + int selectedAction; + boolean signFilterEnabled; + int signId; + boolean danceFilterEnabled; + int danceId; + boolean filterExisting; + boolean invert; + int delay; + + JsonData(int selectedAction, boolean signFilterEnabled, int signId, boolean danceFilterEnabled, int danceId, boolean filterExisting, boolean invert, int delay) { + this.selectedAction = selectedAction; + this.signFilterEnabled = signFilterEnabled; + this.signId = signId; + this.danceFilterEnabled = danceFilterEnabled; + this.danceId = danceId; + this.filterExisting = filterExisting; + this.invert = invert; + this.delay = delay; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByName.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByName.java new file mode 100644 index 00000000..a8dc2ef8 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByName.java @@ -0,0 +1,202 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +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.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +public class WiredEffectUsersByName extends InteractionWiredEffect { + public static final WiredEffectType type = WiredEffectType.USERS_BY_NAME_SELECTOR; + + private String namesText = ""; + private Set usernames = new LinkedHashSet<>(); + private boolean filterExisting = false; + private boolean invert = false; + + public WiredEffectUsersByName(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectUsersByName(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) { + return; + } + + Set result = new LinkedHashSet<>(); + + for (Habbo habbo : room.getHabbos()) { + if (habbo == null || habbo.getHabboInfo() == null || habbo.getRoomUnit() == null) { + continue; + } + + String username = habbo.getHabboInfo().getUsername(); + if (username == null) { + continue; + } + + if (this.usernames.contains(username.trim().toLowerCase(Locale.ROOT))) { + result.add(habbo.getRoomUnit()); + } + } + + Set availableUsers = room.getHabbos().stream() + .filter(habbo -> habbo != null && habbo.getRoomUnit() != null) + .map(Habbo::getRoomUnit) + .collect(Collectors.toCollection(LinkedHashSet::new)); + result = this.applySelectorModifiers(result, availableUsers, ctx.targets().users(), this.filterExisting, this.invert); + + ctx.targets().setUsers(result); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + + this.namesText = this.normalizeNamesText(settings.getStringParam()); + this.usernames = this.parseUsernames(this.namesText); + this.filterExisting = params.length > 0 && params[0] == 1; + this.invert = params.length > 1 && params[1] == 1; + this.setDelay(settings.getDelay()); + return true; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public boolean isSelector() { + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.namesText, this.filterExisting, this.invert, this.getDelay())); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.namesText = this.normalizeNamesText(data.namesText); + this.filterExisting = data.filterExisting; + this.invert = data.invert; + this.setDelay(data.delay); + } else { + this.namesText = this.normalizeNamesText(wiredData); + } + + this.usernames = this.parseUsernames(this.namesText); + } + + @Override + public void onPickUp() { + this.namesText = ""; + this.usernames = new LinkedHashSet<>(); + this.filterExisting = false; + this.invert = false; + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.namesText); + message.appendInt(2); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + private String normalizeNamesText(String value) { + if (value == null || value.trim().isEmpty()) { + return ""; + } + + Set normalizedLines = new LinkedHashSet<>(); + + for (String line : value.split("\\R")) { + String normalized = line.trim(); + if (!normalized.isEmpty()) { + normalizedLines.add(normalized); + } + } + + return normalizedLines.stream().collect(Collectors.joining("\n")); + } + + private Set parseUsernames(String value) { + Set result = new LinkedHashSet<>(); + + if (value == null || value.trim().isEmpty()) { + return result; + } + + for (String line : value.split("\\R")) { + String normalized = line.trim(); + if (!normalized.isEmpty()) { + result.add(normalized.toLowerCase(Locale.ROOT)); + } + } + + return result; + } + + static class JsonData { + String namesText; + boolean filterExisting; + boolean invert; + int delay; + + JsonData(String namesText, boolean filterExisting, boolean invert, int delay) { + this.namesText = namesText; + this.filterExisting = filterExisting; + this.invert = invert; + this.delay = delay; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByType.java new file mode 100644 index 00000000..b9280099 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersByType.java @@ -0,0 +1,167 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitType; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedHashSet; +import java.util.Set; + +public class WiredEffectUsersByType extends InteractionWiredEffect { + private static final int ENTITY_HABBO = 1; + private static final int ENTITY_PET = 2; + private static final int ENTITY_BOT = 4; + + public static final WiredEffectType type = WiredEffectType.USERS_BY_TYPE_SELECTOR; + + private int entityType = ENTITY_HABBO; + private boolean filterExisting = false; + private boolean invert = false; + + public WiredEffectUsersByType(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectUsersByType(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) { + return; + } + + Set result = new LinkedHashSet<>(); + + for (RoomUnit roomUnit : room.getRoomUnits()) { + if (this.matchesType(roomUnit)) { + result.add(roomUnit); + } + } + + result = this.applySelectorModifiers(result, room.getRoomUnits(), ctx.targets().users(), this.filterExisting, this.invert); + + ctx.targets().setUsers(result); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + this.entityType = (params.length > 0) ? this.normalizeEntityType(params[0]) : ENTITY_HABBO; + this.filterExisting = params.length > 1 && params[1] == 1; + this.invert = params.length > 2 && params[2] == 1; + this.setDelay(settings.getDelay()); + return true; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public boolean isSelector() { + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.entityType, this.filterExisting, this.invert, this.getDelay())); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.entityType = this.normalizeEntityType(data.entityType); + this.filterExisting = data.filterExisting; + this.invert = data.invert; + this.setDelay(data.delay); + } + + @Override + public void onPickUp() { + this.entityType = ENTITY_HABBO; + this.filterExisting = false; + this.invert = false; + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(3); + message.appendInt(this.entityType); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + private int normalizeEntityType(int value) { + switch (value) { + case ENTITY_PET: + case ENTITY_BOT: + return value; + default: + return ENTITY_HABBO; + } + } + + private boolean matchesType(RoomUnit roomUnit) { + if (roomUnit == null) { + return false; + } + + RoomUnitType roomUnitType = roomUnit.getRoomUnitType(); + return roomUnitType != null && roomUnitType.getTypeId() == this.entityType; + } + + static class JsonData { + int entityType; + boolean filterExisting; + boolean invert; + int delay; + + JsonData(int entityType, boolean filterExisting, boolean invert, int delay) { + this.entityType = entityType; + this.filterExisting = filterExisting; + this.invert = invert; + this.delay = delay; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersGroup.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersGroup.java new file mode 100644 index 00000000..f74de64a --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersGroup.java @@ -0,0 +1,179 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +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.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +public class WiredEffectUsersGroup extends InteractionWiredEffect { + private static final int GROUP_CURRENT_ROOM = 0; + private static final int GROUP_SELECTED = 1; + + public static final WiredEffectType type = WiredEffectType.USERS_GROUP_SELECTOR; + + private int groupType = GROUP_CURRENT_ROOM; + private int selectedGroupId = 0; + private boolean filterExisting = false; + private boolean invert = false; + + public WiredEffectUsersGroup(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectUsersGroup(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) { + return; + } + + int targetGroupId = this.resolveTargetGroupId(room); + Set result = new LinkedHashSet<>(); + + if (targetGroupId > 0) { + for (Habbo habbo : room.getHabbos()) { + if (habbo == null || habbo.getRoomUnit() == null || habbo.getHabboStats() == null) { + continue; + } + + if (habbo.getHabboStats().hasGuild(targetGroupId)) { + result.add(habbo.getRoomUnit()); + } + } + } + + Set availableUsers = room.getHabbos().stream() + .filter(habbo -> habbo != null && habbo.getRoomUnit() != null) + .map(Habbo::getRoomUnit) + .collect(Collectors.toCollection(LinkedHashSet::new)); + result = this.applySelectorModifiers(result, availableUsers, ctx.targets().users(), this.filterExisting, this.invert); + + ctx.targets().setUsers(result); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + + this.groupType = (params.length > 0) ? this.normalizeGroupType(params[0]) : GROUP_CURRENT_ROOM; + this.selectedGroupId = (params.length > 1) ? Math.max(0, params[1]) : 0; + this.filterExisting = params.length > 2 && params[2] == 1; + this.invert = params.length > 3 && params[3] == 1; + this.setDelay(settings.getDelay()); + return true; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public boolean isSelector() { + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.groupType, this.selectedGroupId, this.filterExisting, this.invert, this.getDelay())); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.groupType = this.normalizeGroupType(data.groupType); + this.selectedGroupId = Math.max(0, data.selectedGroupId); + this.filterExisting = data.filterExisting; + this.invert = data.invert; + this.setDelay(data.delay); + } + + @Override + public void onPickUp() { + this.groupType = GROUP_CURRENT_ROOM; + this.selectedGroupId = 0; + this.filterExisting = false; + this.invert = false; + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(4); + message.appendInt(this.groupType); + message.appendInt(this.selectedGroupId); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + private int resolveTargetGroupId(Room room) { + if (this.groupType == GROUP_CURRENT_ROOM) { + return room.getGuildId(); + } + + return this.selectedGroupId; + } + + private int normalizeGroupType(int value) { + return (value == GROUP_SELECTED) ? GROUP_SELECTED : GROUP_CURRENT_ROOM; + } + + static class JsonData { + int groupType; + int selectedGroupId; + boolean filterExisting; + boolean invert; + int delay; + + JsonData(int groupType, int selectedGroupId, boolean filterExisting, boolean invert, int delay) { + this.groupType = groupType; + this.selectedGroupId = selectedGroupId; + this.filterExisting = filterExisting; + this.invert = invert; + this.delay = delay; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersHandItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersHandItem.java new file mode 100644 index 00000000..e34ef8e7 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersHandItem.java @@ -0,0 +1,145 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedHashSet; +import java.util.Set; + +public class WiredEffectUsersHandItem extends InteractionWiredEffect { + public static final WiredEffectType type = WiredEffectType.USERS_HANDITEM_SELECTOR; + + private int handItemId = 0; + private boolean filterExisting = false; + private boolean invert = false; + + public WiredEffectUsersHandItem(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectUsersHandItem(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) { + return; + } + + Set result = new LinkedHashSet<>(); + + for (RoomUnit roomUnit : room.getRoomUnits()) { + if (roomUnit != null && roomUnit.getHandItem() == this.handItemId) { + result.add(roomUnit); + } + } + + result = this.applySelectorModifiers(result, room.getRoomUnits(), ctx.targets().users(), this.filterExisting, this.invert); + + ctx.targets().setUsers(result); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + + this.handItemId = (params.length > 0) ? Math.max(0, params[0]) : 0; + this.filterExisting = params.length > 1 && params[1] == 1; + this.invert = params.length > 2 && params[2] == 1; + this.setDelay(settings.getDelay()); + + return true; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public boolean isSelector() { + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.handItemId, this.filterExisting, this.invert, this.getDelay())); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.handItemId = Math.max(0, data.handItemId); + this.filterExisting = data.filterExisting; + this.invert = data.invert; + this.setDelay(data.delay); + } + + @Override + public void onPickUp() { + this.handItemId = 0; + this.filterExisting = false; + this.invert = false; + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(3); + message.appendInt(this.handItemId); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + static class JsonData { + int handItemId; + boolean filterExisting; + boolean invert; + int delay; + + JsonData(int handItemId, boolean filterExisting, boolean invert, int delay) { + this.handItemId = handItemId; + this.filterExisting = filterExisting; + this.invert = invert; + this.delay = delay; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java index 7c63b2fb..db19bea6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersNeighborhood.java @@ -6,7 +6,6 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; -import com.eu.habbo.habbohotel.rooms.RoomUnitType; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.core.WiredContext; @@ -43,8 +42,8 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { private int sourceType = SOURCE_USER_TRIGGER; private boolean filterExisting = false; private boolean invert = false; - private boolean excludeBots = false; - private boolean excludePets = false; + private int targetOffsetX = 0; + private int targetOffsetY = 0; private List tileOffsets = new ArrayList<>(); private List pickedFurniIds = new ArrayList<>(); @@ -77,13 +76,11 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { tileOffsets.stream().map(o -> o[0] + "," + o[1]).collect(Collectors.joining(";")), filterExisting, invert); - // Apply tile offsets relative to each source position. - // The offsets define a neighborhood pattern around the source furni/user. Set targetTiles = new HashSet<>(); for (int[] src : sourcePositions) { for (int[] offset : tileOffsets) { - int tx = src[0] + offset[0]; - int ty = src[1] + offset[1]; + int tx = src[0] + (offset[0] - this.targetOffsetX); + int ty = src[1] + (offset[1] - this.targetOffsetY); targetTiles.add(tx + "," + ty); } } @@ -92,22 +89,17 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { List result = new ArrayList<>(); for (RoomUnit unit : room.getRoomUnits()) { - if (excludeBots && unit.getRoomUnitType() == RoomUnitType.BOT) continue; - if (excludePets && unit.getRoomUnitType() == RoomUnitType.PET) continue; - String pos = unit.getX() + "," + unit.getY(); boolean onTile = targetTiles.contains(pos); LOGGER.debug("[Neighborhood] Unit id={} type={} pos={} onTile={}", unit.getId(), unit.getRoomUnitType(), pos, onTile); - if (invert ? !onTile : onTile) { + if (onTile) { result.add(unit); } } - if (filterExisting) { - result.retainAll(ctx.targets().users()); - } + result = new ArrayList<>(this.applySelectorModifiers(result, room.getRoomUnits(), ctx.targets().users(), filterExisting, invert)); LOGGER.debug("[Neighborhood] Result: {} users selected", result.size()); @@ -119,24 +111,46 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { } private List resolveSourcePositions(WiredContext ctx, Room room) { - - if (isUserGroup(sourceType)) { - // Prefer the event tile for user-based sources because during walk-on/walk-off - // events the user's position (getX/getY) hasn't been updated yet (stale position). - // The event tile correctly represents where the triggering action occurred. - if (ctx.tile().isPresent()) { - return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y }); - } - List positions = ctx.targets().users().stream() - .map(u -> new int[]{ u.getX(), u.getY() }) - .collect(Collectors.toList()); - if (positions.isEmpty()) { - ctx.actor().ifPresent(a -> positions.add(new int[]{ a.getX(), a.getY() })); - } - return positions; - } - switch (sourceType) { + case SOURCE_USER_TRIGGER: { + if (ctx.tile().isPresent()) { + return Collections.singletonList(new int[]{ ctx.tile().get().x, ctx.tile().get().y }); + } + + return ctx.actor() + .map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() })) + .orElse(Collections.emptyList()); + } + case SOURCE_USER_SIGNAL: { + List positions = ctx.targets().users().stream() + .map(user -> new int[]{ user.getX(), user.getY() }) + .collect(Collectors.toList()); + + if (!positions.isEmpty()) { + return positions; + } + + return ctx.actor() + .map(actor -> Collections.singletonList(new int[]{ actor.getX(), actor.getY() })) + .orElse(Collections.emptyList()); + } + case SOURCE_USER_CLICKED: { + if (ctx.event().getTargetUnit().isPresent()) { + RoomUnit targetUnit = ctx.event().getTargetUnit().get(); + + return Collections.singletonList(new int[]{ targetUnit.getX(), targetUnit.getY() }); + } + + List positions = ctx.targets().users().stream() + .map(user -> new int[]{ user.getX(), user.getY() }) + .collect(Collectors.toList()); + + if (!positions.isEmpty()) { + return positions; + } + + return Collections.emptyList(); + } case SOURCE_FURNI_TRIGGER: { return ctx.sourceItem() .map(i -> Collections.singletonList(new int[]{ i.getX(), i.getY() })) @@ -150,9 +164,17 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { .collect(Collectors.toList()); } case SOURCE_FURNI_SIGNAL: { - return ctx.targets().items().stream() + List positions = ctx.targets().items().stream() .map(i -> new int[]{ i.getX(), i.getY() }) .collect(Collectors.toList()); + + if (!positions.isEmpty()) { + return positions; + } + + return ctx.sourceItem() + .map(item -> Collections.singletonList(new int[]{ item.getX(), item.getY() })) + .orElse(Collections.emptyList()); } default: return Collections.emptyList(); @@ -169,8 +191,8 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { this.sourceType = params[0]; this.filterExisting = params.length > 1 && params[1] == 1; this.invert = params.length > 2 && params[2] == 1; - this.excludeBots = params.length > 3 && params[3] == 1; - this.excludePets = params.length > 4 && params[4] == 1; + this.targetOffsetX = params.length > 3 ? params[3] : 0; + this.targetOffsetY = params.length > 4 ? params[4] : 0; this.tileOffsets = new ArrayList<>(); if (params.length > 5) { @@ -218,8 +240,8 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { message.appendInt(sourceType); message.appendInt(filterExisting ? 1 : 0); message.appendInt(invert ? 1 : 0); - message.appendInt(excludeBots ? 1 : 0); - message.appendInt(excludePets ? 1 : 0); + message.appendInt(targetOffsetX); + message.appendInt(targetOffsetY); message.appendInt(tileOffsets.size()); for (int[] offset : tileOffsets) { message.appendInt(offset[0]); @@ -243,7 +265,7 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { @Override public String getWiredData() { return WiredManager.getGson().toJson( - new JsonData(sourceType, filterExisting, invert, excludeBots, excludePets, tileOffsets, pickedFurniIds, getDelay())); + new JsonData(sourceType, filterExisting, invert, targetOffsetX, targetOffsetY, tileOffsets, pickedFurniIds, getDelay())); } @Override @@ -254,8 +276,8 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { this.sourceType = data.sourceType; this.filterExisting = data.filterExisting; this.invert = data.invert; - this.excludeBots = data.excludeBots; - this.excludePets = data.excludePets; + this.targetOffsetX = data.targetOffsetX; + this.targetOffsetY = data.targetOffsetY; this.tileOffsets = data.tileOffsets != null ? data.tileOffsets : new ArrayList<>(); this.pickedFurniIds = data.pickedFurniIds != null ? data.pickedFurniIds : new ArrayList<>(); this.setDelay(data.delay); @@ -267,8 +289,8 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { this.sourceType = SOURCE_USER_TRIGGER; this.filterExisting = false; this.invert = false; - this.excludeBots = false; - this.excludePets = false; + this.targetOffsetX = 0; + this.targetOffsetY = 0; this.tileOffsets = new ArrayList<>(); this.pickedFurniIds = new ArrayList<>(); this.setDelay(0); @@ -281,20 +303,20 @@ public class WiredEffectUsersNeighborhood extends InteractionWiredEffect { int sourceType; boolean filterExisting; boolean invert; - boolean excludeBots; - boolean excludePets; + int targetOffsetX; + int targetOffsetY; List tileOffsets; List pickedFurniIds; int delay; JsonData(int sourceType, boolean filterExisting, boolean invert, - boolean excludeBots, boolean excludePets, + int targetOffsetX, int targetOffsetY, List tileOffsets, List pickedFurniIds, int delay) { this.sourceType = sourceType; this.filterExisting = filterExisting; this.invert = invert; - this.excludeBots = excludeBots; - this.excludePets = excludePets; + this.targetOffsetX = targetOffsetX; + this.targetOffsetY = targetOffsetY; this.tileOffsets = tileOffsets; this.pickedFurniIds = pickedFurniIds; this.delay = delay; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersOnFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersOnFurni.java new file mode 100644 index 00000000..149ae0cb --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersOnFurni.java @@ -0,0 +1,273 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class WiredEffectUsersOnFurni extends InteractionWiredEffect { + public static final WiredEffectType type = WiredEffectType.USERS_ON_FURNI_SELECTOR; + + private final Set items = new LinkedHashSet<>(); + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private boolean filterExisting = false; + private boolean invert = false; + + public WiredEffectUsersOnFurni(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectUsersOnFurni(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null || room.getLayout() == null) { + ctx.targets().setUsers(Collections.emptySet()); + return; + } + + this.refresh(room); + + List sourceItems = WiredSourceUtil.resolveItems(ctx, this.furniSource, this.items); + if (sourceItems.isEmpty()) { + ctx.targets().setUsers(Collections.emptySet()); + return; + } + + Set result = new LinkedHashSet<>(); + + for (HabboItem sourceItem : sourceItems) { + result.addAll(this.resolveUnitsOnItem(room, sourceItem)); + } + + result = this.applySelectorModifiers(result, room.getRoomUnits(), ctx.targets().users(), this.filterExisting, this.invert); + + ctx.targets().setUsers(result); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + + this.furniSource = (params.length > 0) ? this.normalizeFurniSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER; + this.filterExisting = params.length > 1 && params[1] == 1; + this.invert = params.length > 2 && params[2] == 1; + + int count = settings.getFurniIds().length; + if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + return false; + } + + if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + + this.items.clear(); + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + + if (room == null) { + return false; + } + + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (item != null) { + this.items.add(item); + } + } + } + + this.setDelay(settings.getDelay()); + return true; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public boolean isSelector() { + return true; + } + + @Override + public String getWiredData() { + this.refresh(Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId())); + + return WiredManager.getGson().toJson(new JsonData( + this.furniSource, + this.filterExisting, + this.invert, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()), + this.getDelay() + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.furniSource = this.normalizeFurniSource(data.furniSource); + this.filterExisting = data.filterExisting; + this.invert = data.invert; + this.setDelay(data.delay); + + if (room == null || data.itemIds == null) { + return; + } + + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + + if (item != null) { + this.items.add(item); + } + } + } + + @Override + public void onPickUp() { + this.items.clear(); + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.filterExisting = false; + this.invert = false; + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(room); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(this.items.size()); + + for (HabboItem item : this.items) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(3); + message.appendInt(this.furniSource); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + private Set resolveUnitsOnItem(Room room, HabboItem sourceItem) { + Set result = new LinkedHashSet<>(); + + if (sourceItem == null) { + return result; + } + + RoomTile baseTile = room.getLayout().getTile(sourceItem.getX(), sourceItem.getY()); + if (baseTile == null) { + return result; + } + + Set occupiedTiles = room.getLayout().getTilesAt(baseTile, sourceItem.getBaseItem().getWidth(), sourceItem.getBaseItem().getLength(), sourceItem.getRotation()); + if (occupiedTiles == null) { + return result; + } + + for (RoomTile tile : occupiedTiles) { + if (tile == null) { + continue; + } + + result.addAll(room.getUnitManager().getRoomUnitsAt(tile)); + } + + return result; + } + + private void refresh(Room room) { + Set invalidItems = new LinkedHashSet<>(); + + if (room == null) { + invalidItems.addAll(this.items); + } else { + for (HabboItem item : this.items) { + if (room.getHabboItem(item.getId()) == null) { + invalidItems.add(item); + } + } + } + + this.items.removeAll(invalidItems); + } + + private int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_TRIGGER: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + static class JsonData { + int furniSource; + boolean filterExisting; + boolean invert; + List itemIds; + int delay; + + JsonData(int furniSource, boolean filterExisting, boolean invert, List itemIds, int delay) { + this.furniSource = furniSource; + this.filterExisting = filterExisting; + this.invert = invert; + this.itemIds = itemIds; + this.delay = delay; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersSignal.java new file mode 100644 index 00000000..1ce2eaba --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersSignal.java @@ -0,0 +1,138 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +public class WiredEffectUsersSignal extends InteractionWiredEffect { + public static final WiredEffectType type = WiredEffectType.USERS_SIGNAL_SELECTOR; + + private boolean filterExisting = false; + private boolean invert = false; + + public WiredEffectUsersSignal(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectUsersSignal(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) { + return; + } + + Set result = new LinkedHashSet<>(); + + if (ctx.eventType() == WiredEvent.Type.SIGNAL_RECEIVED) { + List signalUsers = WiredSourceUtil.resolveUsers(ctx, WiredSourceUtil.SOURCE_SIGNAL); + result.addAll(signalUsers); + + result = this.applySelectorModifiers(result, room.getRoomUnits(), ctx.targets().users(), this.filterExisting, this.invert); + } + + ctx.targets().setUsers(result); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + this.filterExisting = params.length > 0 && params[0] == 1; + this.invert = params.length > 1 && params[1] == 1; + this.setDelay(settings.getDelay()); + return true; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public boolean isSelector() { + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.filterExisting, this.invert, this.getDelay())); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.filterExisting = data.filterExisting; + this.invert = data.invert; + this.setDelay(data.delay); + } + + @Override + public void onPickUp() { + this.filterExisting = false; + this.invert = false; + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(2); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + static class JsonData { + boolean filterExisting; + boolean invert; + int delay; + + JsonData(boolean filterExisting, boolean invert, int delay) { + this.filterExisting = filterExisting; + this.invert = invert; + this.delay = delay; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersTeam.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersTeam.java new file mode 100644 index 00000000..8b16f73f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersTeam.java @@ -0,0 +1,177 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +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.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedHashSet; +import java.util.Set; + +public class WiredEffectUsersTeam extends InteractionWiredEffect { + private static final int TEAM_ANY = 0; + + public static final WiredEffectType type = WiredEffectType.USERS_TEAM_SELECTOR; + + private int teamType = TEAM_ANY; + private boolean filterExisting = false; + private boolean invert = false; + + public WiredEffectUsersTeam(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectUsersTeam(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + if (room == null) { + return; + } + + Set result = new LinkedHashSet<>(); + + for (RoomUnit roomUnit : room.getRoomUnits()) { + if (this.matchesTeam(room, roomUnit)) { + result.add(roomUnit); + } + } + + result = this.applySelectorModifiers(result, room.getRoomUnits(), ctx.targets().users(), this.filterExisting, this.invert); + + ctx.targets().setUsers(result); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + this.teamType = (params.length > 0) ? this.normalizeTeamType(params[0]) : TEAM_ANY; + this.filterExisting = params.length > 1 && params[1] == 1; + this.invert = params.length > 2 && params[2] == 1; + this.setDelay(settings.getDelay()); + return true; + } + + @Override + public WiredEffectType getType() { + return type; + } + + @Override + public boolean isSelector() { + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.teamType, this.filterExisting, this.invert, this.getDelay())); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || !wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.teamType = this.normalizeTeamType(data.teamType); + this.filterExisting = data.filterExisting; + this.invert = data.invert; + this.setDelay(data.delay); + } + + @Override + public void onPickUp() { + this.teamType = TEAM_ANY; + this.filterExisting = false; + this.invert = false; + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(3); + message.appendInt(this.teamType); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + private int normalizeTeamType(int value) { + if (value == TEAM_ANY) { + return TEAM_ANY; + } + + GameTeamColors teamColor = GameTeamColors.fromType(value); + return (teamColor.type >= GameTeamColors.RED.type && teamColor.type <= GameTeamColors.YELLOW.type) + ? teamColor.type + : TEAM_ANY; + } + + private boolean matchesTeam(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) { + return false; + } + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo == null || habbo.getHabboInfo() == null) { + return false; + } + + GamePlayer gamePlayer = habbo.getHabboInfo().getGamePlayer(); + if (gamePlayer == null || gamePlayer.getTeamColor() == null || gamePlayer.getTeamColor() == GameTeamColors.NONE) { + return false; + } + + return (this.teamType == TEAM_ANY) || gamePlayer.getTeamColor().type == this.teamType; + } + + static class JsonData { + int teamType; + boolean filterExisting; + boolean invert; + int delay; + + JsonData(int teamType, boolean filterExisting, boolean invert, int delay) { + this.teamType = teamType; + this.filterExisting = filterExisting; + this.invert = invert; + this.delay = delay; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersWithVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersWithVariable.java new file mode 100644 index 00000000..1046ad42 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectUsersWithVariable.java @@ -0,0 +1,29 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.wired.WiredEffectType; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredEffectUsersWithVariable extends WiredEffectVariableSelectorBase { + public static final WiredEffectType type = WiredEffectType.USERS_WITH_VAR_SELECTOR; + + public WiredEffectUsersWithVariable(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredEffectUsersWithVariable(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + protected int getVariableTargetType() { + return TARGET_USER; + } + + @Override + public WiredEffectType getType() { + return type; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java new file mode 100644 index 00000000..11916849 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/selector/WiredEffectVariableSelectorBase.java @@ -0,0 +1,862 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.selector; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame; +import com.eu.habbo.habbohotel.games.freeze.FreezeGame; +import com.eu.habbo.habbohotel.games.wired.WiredGame; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWired; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomRightLevels; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.DanceType; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredEffectType; +import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; +import com.eu.habbo.habbohotel.wired.core.WiredInternalVariableSupport; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredSaveException; +import com.eu.habbo.util.HotelDateTimeUtil; +import gnu.trove.set.hash.THashSet; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; + +public abstract class WiredEffectVariableSelectorBase extends InteractionWiredEffect { + protected static final int TARGET_USER = 0; + protected static final int TARGET_FURNI = 1; + protected static final int TARGET_CONTEXT = 2; + protected static final int TARGET_ROOM = 3; + + protected static final int REFERENCE_CONSTANT = 0; + protected static final int REFERENCE_VARIABLE = 1; + + protected static final int SOURCE_SECONDARY_SELECTED = 101; + + protected static final int COMPARISON_GREATER_THAN = 0; + protected static final int COMPARISON_GREATER_THAN_OR_EQUAL = 1; + protected static final int COMPARISON_EQUAL = 2; + protected static final int COMPARISON_LESS_THAN_OR_EQUAL = 3; + protected static final int COMPARISON_LESS_THAN = 4; + protected static final int COMPARISON_NOT_EQUAL = 5; + + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + private static final String INTERNAL_TOKEN_PREFIX = "internal:"; + private static final String DELIM = "\t"; + + protected boolean selectByValue = false; + protected int comparison = COMPARISON_EQUAL; + protected int referenceMode = REFERENCE_CONSTANT; + protected int referenceConstantValue = 0; + protected int referenceTargetType = TARGET_USER; + protected int referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER; + protected int referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + protected boolean filterExisting = false; + protected boolean invert = false; + protected String variableToken = ""; + protected int variableItemId = 0; + protected String referenceVariableToken = ""; + protected int referenceVariableItemId = 0; + protected final THashSet referenceSelectedItems = new THashSet<>(); + + protected WiredEffectVariableSelectorBase(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + protected WiredEffectVariableSelectorBase(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + protected abstract int getVariableTargetType(); + + @Override + public void execute(WiredContext ctx) { + Room room = ctx.room(); + + if (room == null || this.variableToken == null || this.variableToken.isEmpty()) { + return; + } + + if (this.getVariableTargetType() == TARGET_FURNI) { + LinkedHashSet matchedItems = new LinkedHashSet<>(); + + for (HabboItem item : this.getSelectableFloorItems(room, ctx)) { + if (item == null) continue; + if (!this.matchesFurni(room, item, ctx)) continue; + + matchedItems.add(item); + } + + LinkedHashSet result = this.applySelectorModifiers(matchedItems, this.getSelectableFloorItems(room, ctx), ctx.targets().items(), this.filterExisting, this.invert); + ctx.targets().setItems(result); + return; + } + + LinkedHashSet matchedUsers = new LinkedHashSet<>(); + + for (RoomUnit roomUnit : room.getRoomUnits()) { + if (roomUnit == null) continue; + if (!this.matchesUser(room, roomUnit, ctx)) continue; + + matchedUsers.add(roomUnit); + } + + LinkedHashSet result = this.applySelectorModifiers(matchedUsers, room.getRoomUnits(), ctx.targets().users(), this.filterExisting, this.invert); + ctx.targets().setUsers(result); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) throws WiredSaveException { + Room room = this.getRoom(); + if (room == null) return false; + + int[] params = settings.getIntParams(); + String[] stringParts = parseStringData(settings.getStringParam()); + + boolean nextSelectByValue = param(params, 0, 0) == 1; + int nextComparison = normalizeComparison(param(params, 1, COMPARISON_EQUAL)); + int nextReferenceMode = normalizeReferenceMode(param(params, 2, REFERENCE_CONSTANT)); + int nextReferenceConstantValue = param(params, 3, 0); + int nextReferenceTargetType = normalizeReferenceTargetType(param(params, 4, TARGET_USER)); + int nextReferenceUserSource = normalizeUserSource(param(params, 5, WiredSourceUtil.SOURCE_TRIGGER)); + int nextReferenceFurniSource = normalizeReferenceFurniSource(param(params, 6, WiredSourceUtil.SOURCE_TRIGGER)); + boolean nextFilterExisting = param(params, 7, 0) == 1; + boolean nextInvert = param(params, 8, 0) == 1; + String nextVariableToken = normalizeVariableToken((stringParts.length > 0) ? stringParts[0] : settings.getStringParam()); + String nextReferenceVariableToken = normalizeVariableToken((stringParts.length > 1) ? stringParts[1] : ""); + + if (!this.isValidMainVariable(room, nextVariableToken, nextSelectByValue)) return false; + if (nextSelectByValue && nextReferenceMode == REFERENCE_VARIABLE && !this.isValidReference(room, nextReferenceTargetType, nextReferenceVariableToken)) return false; + + List nextReferenceItems = new ArrayList<>(); + + if (nextSelectByValue && nextReferenceMode == REFERENCE_VARIABLE && nextReferenceTargetType == TARGET_FURNI && nextReferenceFurniSource == SOURCE_SECONDARY_SELECTED) { + int[] furniIds = settings.getFurniIds(); + if (furniIds.length > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; + + for (int furniId : furniIds) { + HabboItem item = room.getHabboItem(furniId); + if (item != null) nextReferenceItems.add(item); + } + } + + this.referenceSelectedItems.clear(); + this.referenceSelectedItems.addAll(nextReferenceItems); + this.selectByValue = nextSelectByValue; + this.comparison = nextComparison; + this.referenceMode = nextReferenceMode; + this.referenceConstantValue = nextReferenceConstantValue; + this.referenceTargetType = nextReferenceTargetType; + this.referenceUserSource = nextReferenceUserSource; + this.referenceFurniSource = nextReferenceFurniSource; + this.filterExisting = nextFilterExisting; + this.invert = nextInvert; + this.setVariableToken(nextVariableToken); + this.setReferenceVariableToken(nextReferenceVariableToken); + this.setDelay(settings.getDelay()); + + return true; + } + + @Override + public boolean isSelector() { + return true; + } + + @Override + public String getWiredData() { + this.refreshReferenceItems(); + + return WiredManager.getGson().toJson(new JsonData( + this.selectByValue, + this.comparison, + this.referenceMode, + this.referenceConstantValue, + this.referenceTargetType, + this.referenceUserSource, + this.referenceFurniSource, + this.filterExisting, + this.invert, + this.variableToken, + this.variableItemId, + this.referenceVariableToken, + this.referenceVariableItemId, + this.toIds(this.referenceSelectedItems), + this.getDelay() + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) return; + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) return; + + this.selectByValue = data.selectByValue; + this.comparison = normalizeComparison(data.comparison); + this.referenceMode = normalizeReferenceMode(data.referenceMode); + this.referenceConstantValue = data.referenceConstantValue; + this.referenceTargetType = normalizeReferenceTargetType(data.referenceTargetType); + this.referenceUserSource = normalizeUserSource(data.referenceUserSource); + this.referenceFurniSource = normalizeReferenceFurniSource(data.referenceFurniSource); + this.filterExisting = data.filterExisting; + this.invert = data.invert; + this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); + this.setReferenceVariableToken(normalizeVariableToken((data.referenceVariableToken != null) ? data.referenceVariableToken : ((data.referenceVariableItemId > 0) ? String.valueOf(data.referenceVariableItemId) : ""))); + this.setDelay(data.delay); + + if (room == null || data.selectedItemIds == null) return; + + for (Integer itemId : data.selectedItemIds) { + if (itemId == null || itemId <= 0) continue; + + HabboItem item = room.getHabboItem(itemId); + if (item != null) this.referenceSelectedItems.add(item); + } + } + + @Override + public void onPickUp() { + this.selectByValue = false; + this.comparison = COMPARISON_EQUAL; + this.referenceMode = REFERENCE_CONSTANT; + this.referenceConstantValue = 0; + this.referenceTargetType = TARGET_USER; + this.referenceUserSource = WiredSourceUtil.SOURCE_TRIGGER; + this.referenceFurniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.filterExisting = false; + this.invert = false; + this.referenceSelectedItems.clear(); + this.setVariableToken(""); + this.setReferenceVariableToken(""); + this.setDelay(0); + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refreshReferenceItems(); + + List serializedItems = new ArrayList<>(); + if (this.selectByValue && this.referenceMode == REFERENCE_VARIABLE && this.referenceTargetType == TARGET_FURNI && this.referenceFurniSource == SOURCE_SECONDARY_SELECTED) { + serializedItems.addAll(this.referenceSelectedItems); + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(serializedItems.size()); + + for (HabboItem item : serializedItems) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.serializeStringData()); + message.appendInt(9); + message.appendInt(this.selectByValue ? 1 : 0); + message.appendInt(this.comparison); + message.appendInt(this.referenceMode); + message.appendInt(this.referenceConstantValue); + message.appendInt(this.referenceTargetType); + message.appendInt(this.referenceUserSource); + message.appendInt(this.referenceFurniSource); + message.appendInt(this.filterExisting ? 1 : 0); + message.appendInt(this.invert ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(this.getDelay()); + message.appendInt(0); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public boolean requiresTriggeringUser() { + return this.selectByValue && this.referenceMode == REFERENCE_VARIABLE && this.referenceTargetType == TARGET_USER && this.referenceUserSource == WiredSourceUtil.SOURCE_TRIGGER; + } + + private boolean matchesUser(Room room, RoomUnit roomUnit, WiredContext ctx) { + if (!this.selectByValue) return this.hasUserVariable(room, roomUnit); + + Integer currentValue = this.readUserValue(room, roomUnit); + Integer referenceValue = this.resolveReferenceValue(ctx, room, roomUnit != null ? roomUnit.getId() : 0, TARGET_USER, -1); + + return this.matchesComparison(currentValue, referenceValue); + } + + private boolean matchesFurni(Room room, HabboItem item, WiredContext ctx) { + if (!this.selectByValue) return this.hasFurniVariable(room, item); + + Integer currentValue = this.readFurniValue(room, item); + Integer referenceValue = this.resolveReferenceValue(ctx, room, item != null ? item.getId() : 0, TARGET_FURNI, -1); + + return this.matchesComparison(currentValue, referenceValue); + } + + private boolean hasUserVariable(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) return false; + + if (isCustomVariableToken(this.variableToken)) { + Habbo habbo = room.getHabbo(roomUnit); + return habbo != null && room.getUserVariableManager().hasVariable(habbo.getHabboInfo().getId(), this.variableItemId); + } + + return isInternalVariableToken(this.variableToken) && this.hasUserInternalVariable(room, roomUnit, getInternalVariableKey(this.variableToken)); + } + + private boolean hasFurniVariable(Room room, HabboItem item) { + if (room == null || item == null) return false; + + if (isCustomVariableToken(this.variableToken)) { + return room.getFurniVariableManager().hasVariable(item.getId(), this.variableItemId); + } + + return isInternalVariableToken(this.variableToken) && this.hasFurniInternalVariable(item, getInternalVariableKey(this.variableToken)); + } + + private Integer resolveReferenceValue(WiredContext ctx, Room room, int destinationEntityId, int destinationTargetType, int destinationIndex) { + if (!this.selectByValue || this.referenceMode != REFERENCE_VARIABLE) return this.referenceConstantValue; + + ReferenceSnapshot snapshot = this.resolveReferences(ctx, room); + if (snapshot == null || snapshot.isEmpty()) return null; + if (snapshot.targetType == destinationTargetType && snapshot.values.containsKey(destinationEntityId)) return snapshot.values.get(destinationEntityId); + if (destinationIndex >= 0 && destinationIndex < snapshot.values.size()) return new ArrayList<>(snapshot.values.values()).get(destinationIndex); + + return new ArrayList<>(snapshot.values.values()).get(0); + } + + private ReferenceSnapshot resolveReferences(WiredContext ctx, Room room) { + return switch (this.referenceTargetType) { + case TARGET_FURNI -> this.furniReferences(ctx, room); + case TARGET_CONTEXT -> this.contextReferences(ctx, room); + case TARGET_ROOM -> this.roomReferences(room); + default -> this.userReferences(ctx, room); + }; + } + + private ReferenceSnapshot userReferences(WiredContext ctx, Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_USER); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseUserInternalReference(key)) return null; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.referenceUserSource)) { + Integer value = this.readUserInternalValue(room, roomUnit, key); + if (value != null && roomUnit != null) snapshot.add(roomUnit.getId(), value); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, this.referenceUserSource)) { + if (roomUnit == null) continue; + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo != null) snapshot.add(roomUnit.getId(), room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.referenceVariableItemId)); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + private ReferenceSnapshot furniReferences(WiredContext ctx, Room room) { + int source = (this.referenceFurniSource == SOURCE_SECONDARY_SELECTED) ? WiredSourceUtil.SOURCE_SELECTED : this.referenceFurniSource; + if (source == WiredSourceUtil.SOURCE_SELECTED) this.refreshReferenceItems(); + + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_FURNI); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseFurniInternalReference(key)) return null; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, source, this.referenceSelectedItems)) { + Integer value = this.readFurniInternalValue(room, item, key); + if (value != null && item != null) snapshot.add(item.getId(), value); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + for (HabboItem item : WiredSourceUtil.resolveItems(ctx, source, this.referenceSelectedItems)) { + if (item != null) snapshot.add(item.getId(), room.getFurniVariableManager().getCurrentValue(item.getId(), this.referenceVariableItemId)); + } + + return snapshot.isEmpty() ? null : snapshot; + } + + private ReferenceSnapshot roomReferences(Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_ROOM); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseRoomInternalReference(key)) return null; + + Integer value = this.readRoomInternalValue(room, key); + if (value == null) return null; + + snapshot.add(room.getId(), value); + return snapshot; + } + + WiredVariableDefinitionInfo definition = room.getRoomVariableManager().getDefinitionInfo(this.referenceVariableItemId); + if (definition == null || !definition.hasValue()) return null; + + snapshot.add(room.getId(), room.getRoomVariableManager().getCurrentValue(this.referenceVariableItemId)); + return snapshot; + } + + private ReferenceSnapshot contextReferences(WiredContext ctx, Room room) { + ReferenceSnapshot snapshot = new ReferenceSnapshot(TARGET_CONTEXT); + + if (isInternalVariableToken(this.referenceVariableToken)) { + String key = getInternalVariableKey(this.referenceVariableToken); + if (!canUseContextInternalReference(key)) return null; + + Integer value = WiredInternalVariableSupport.readContextValue(ctx, key); + if (value == null) return null; + + snapshot.add(this.referenceVariableItemId > 0 ? this.referenceVariableItemId : (room != null ? room.getId() : 0), value); + return snapshot; + } + + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, this.referenceVariableItemId); + if (definition == null || !definition.hasValue() || !WiredContextVariableSupport.hasVariable(ctx, this.referenceVariableItemId)) return null; + + Integer value = WiredContextVariableSupport.getCurrentValue(ctx, this.referenceVariableItemId); + if (value == null) return null; + + snapshot.add(this.referenceVariableItemId, value); + return snapshot; + } + + private boolean isValidMainVariable(Room room, String token, boolean requireValue) { + if (token == null || token.isEmpty()) return false; + + int targetType = this.getVariableTargetType(); + + if (isInternalVariableToken(token)) { + String key = getInternalVariableKey(token); + return targetType == TARGET_FURNI + ? (requireValue ? canUseFurniInternalReference(key) : this.hasFurniInternalKey(key)) + : (requireValue ? canUseUserInternalReference(key) : this.hasUserInternalKey(key)); + } + + if (targetType == TARGET_FURNI) { + WiredVariableDefinitionInfo definition = (room != null) ? room.getFurniVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + return definition != null && (!requireValue || definition.hasValue()); + } + + WiredVariableDefinitionInfo definition = (room != null) ? room.getUserVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + return definition != null && (!requireValue || definition.hasValue()); + } + + private boolean isValidReference(Room room, int targetType, String token) { + if (token == null || token.isEmpty()) return false; + + if (isInternalVariableToken(token)) { + String key = getInternalVariableKey(token); + return switch (targetType) { + case TARGET_FURNI -> canUseFurniInternalReference(key); + case TARGET_CONTEXT -> canUseContextInternalReference(key); + case TARGET_ROOM -> canUseRoomInternalReference(key); + default -> canUseUserInternalReference(key); + }; + } + + return switch (targetType) { + case TARGET_FURNI -> { + WiredVariableDefinitionInfo definition = (room != null) ? room.getFurniVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + yield definition != null && definition.hasValue(); + } + case TARGET_CONTEXT -> this.isValidContextCustomReference(room, getCustomItemId(token)); + case TARGET_ROOM -> { + WiredVariableDefinitionInfo definition = (room != null) ? room.getRoomVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + yield definition != null && definition.hasValue(); + } + default -> { + WiredVariableDefinitionInfo definition = (room != null) ? room.getUserVariableManager().getDefinitionInfo(getCustomItemId(token)) : null; + yield definition != null && definition.hasValue(); + } + }; + } + + private boolean isValidContextCustomReference(Room room, int variableItemId) { + WiredVariableDefinitionInfo definition = WiredContextVariableSupport.getDefinitionInfo(room, variableItemId); + return definition != null && definition.hasValue(); + } + + private boolean matchesComparison(Integer currentValue, Integer referenceValue) { + if (currentValue == null || referenceValue == null) return false; + + return switch (this.comparison) { + case COMPARISON_GREATER_THAN -> currentValue > referenceValue; + case COMPARISON_GREATER_THAN_OR_EQUAL -> currentValue >= referenceValue; + case COMPARISON_LESS_THAN_OR_EQUAL -> currentValue <= referenceValue; + case COMPARISON_LESS_THAN -> currentValue < referenceValue; + case COMPARISON_NOT_EQUAL -> !currentValue.equals(referenceValue); + default -> currentValue.equals(referenceValue); + }; + } + + private Integer readUserValue(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + return canUseUserInternalReference(key) ? this.readUserInternalValue(room, roomUnit, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getUserVariableManager().getDefinitionInfo(this.variableItemId); + if (definition == null || !definition.hasValue()) return null; + + Habbo habbo = room.getHabbo(roomUnit); + return (habbo != null) ? room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), this.variableItemId) : null; + } + + private Integer readFurniValue(Room room, HabboItem item) { + if (room == null || item == null) return null; + + if (isInternalVariableToken(this.variableToken)) { + String key = getInternalVariableKey(this.variableToken); + return canUseFurniInternalReference(key) ? this.readFurniInternalValue(room, item, key) : null; + } + + WiredVariableDefinitionInfo definition = room.getFurniVariableManager().getDefinitionInfo(this.variableItemId); + return (definition != null && definition.hasValue()) ? room.getFurniVariableManager().getCurrentValue(item.getId(), this.variableItemId) : null; + } + + private boolean hasUserInternalVariable(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.hasUserValue(room, roomUnit, key); + } + + private boolean hasFurniInternalVariable(HabboItem item, String key) { + return WiredInternalVariableSupport.hasFurniValue(item, key); + } + + private boolean hasUserInternalKey(String key) { + return WiredInternalVariableSupport.canUseUserReference(key); + } + + private boolean hasFurniInternalKey(String key) { + return WiredInternalVariableSupport.canUseFurniReference(key) || "@wallitem_offset".equals(WiredInternalVariableSupport.normalizeKey(key)); + } + + private boolean hasRoomEntryMethod(Habbo habbo) { + if (habbo == null) return false; + + String roomEntryMethod = habbo.getHabboInfo().getRoomEntryMethod(); + return roomEntryMethod != null && !roomEntryMethod.trim().isEmpty() && !"unknown".equalsIgnoreCase(roomEntryMethod); + } + + private Integer readUserInternalValue(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.readUserValue(room, roomUnit, key); + } + + private Integer readFurniInternalValue(Room room, HabboItem item, String key) { + return WiredInternalVariableSupport.readFurniValue(room, item, key); + } + + private Integer readRoomInternalValue(Room room, String key) { + return WiredInternalVariableSupport.readRoomValue(room, key); + } + + private Integer getUserTeamScore(Room room, Habbo habbo) { + if (room == null || habbo == null || habbo.getHabboInfo().getGamePlayer() == null) return null; + + Game game = this.resolveTeamGame(room, habbo); + GamePlayer gamePlayer = habbo.getHabboInfo().getGamePlayer(); + + if (game == null || gamePlayer.getTeamColor() == null) return gamePlayer.getScore(); + + GameTeam team = game.getTeam(gamePlayer.getTeamColor()); + return (team != null) ? team.getTotalScore() : gamePlayer.getScore(); + } + + private Integer getTeamColorId(int effectId) { + TeamEffectData data = this.getTeamEffectData(effectId); + return data == null ? null : data.colorId; + } + + private Integer getTeamTypeId(int effectId) { + TeamEffectData data = this.getTeamEffectData(effectId); + return data == null ? null : data.typeId; + } + + private int getTeamMetric(Room room, GameTeamColors color, boolean score) { + Game game = this.resolveTeamGame(room, null); + if (game == null || color == null) return 0; + + GameTeam team = game.getTeam(color); + if (team == null) return 0; + + return score ? team.getTotalScore() : team.getMembers().size(); + } + + private Game resolveTeamGame(Room room, Habbo habbo) { + if (room == null) return null; + + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getCurrentGame() != null) { + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (game != null) return game; + } + + Game wiredGame = room.getGame(WiredGame.class); + if (wiredGame != null) return wiredGame; + + Game freezeGame = room.getGame(FreezeGame.class); + if (freezeGame != null) return freezeGame; + + return room.getGame(BattleBanzaiGame.class); + } + + private TeamEffectData getTeamEffectData(int effectValue) { + if (effectValue <= 0) return null; + + if (effectValue >= 223 && effectValue <= 226) return new TeamEffectData(effectValue - 222, 0); + if (effectValue >= 33 && effectValue <= 36) return new TeamEffectData(effectValue - 32, 1); + if (effectValue >= 40 && effectValue <= 43) return new TeamEffectData(effectValue - 39, 2); + + return null; + } + + private void refreshReferenceItems() { + THashSet staleItems = new THashSet<>(); + Room room = this.getRoom(); + + if (room == null) { + staleItems.addAll(this.referenceSelectedItems); + } else { + for (HabboItem item : this.referenceSelectedItems) { + if (item == null || item.getRoomId() != room.getId() || room.getHabboItem(item.getId()) == null) { + staleItems.add(item); + } + } + } + + this.referenceSelectedItems.removeAll(staleItems); + } + + private String serializeStringData() { + return (this.variableToken == null ? "" : this.variableToken) + DELIM + (this.referenceVariableToken == null ? "" : this.referenceVariableToken); + } + + private void setVariableToken(String token) { + this.variableToken = normalizeVariableToken(token); + this.variableItemId = getCustomItemId(this.variableToken); + } + + private void setReferenceVariableToken(String token) { + this.referenceVariableToken = normalizeVariableToken(token); + this.referenceVariableItemId = getCustomItemId(this.referenceVariableToken); + } + + private List toIds(THashSet items) { + List ids = new ArrayList<>(); + + for (HabboItem item : items) { + if (item != null) ids.add(item.getId()); + } + + return ids; + } + + private static int normalizeReferenceMode(int value) { + return (value == REFERENCE_VARIABLE) ? REFERENCE_VARIABLE : REFERENCE_CONSTANT; + } + + private static int normalizeReferenceTargetType(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + private static int normalizeReferenceFurniSource(int value) { + return switch (value) { + case SOURCE_SECONDARY_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + } + + private static int normalizeComparison(int value) { + return switch (value) { + case COMPARISON_GREATER_THAN, COMPARISON_GREATER_THAN_OR_EQUAL, COMPARISON_LESS_THAN_OR_EQUAL, COMPARISON_LESS_THAN, COMPARISON_NOT_EQUAL -> value; + default -> COMPARISON_EQUAL; + }; + } + + private static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + protected static boolean isCustomVariableToken(String token) { + return token != null && token.startsWith(CUSTOM_TOKEN_PREFIX); + } + + protected static boolean isInternalVariableToken(String token) { + return token != null && token.startsWith(INTERNAL_TOKEN_PREFIX); + } + + private static int getCustomItemId(String token) { + if (!isCustomVariableToken(token)) return 0; + + try { + return Integer.parseInt(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } catch (NumberFormatException e) { + return 0; + } + } + + private static String getInternalVariableKey(String token) { + return isInternalVariableToken(token) ? WiredInternalVariableSupport.normalizeKey(token.substring(INTERNAL_TOKEN_PREFIX.length())) : ""; + } + + protected static String normalizeVariableToken(String token) { + if (token == null) return ""; + + String normalized = token.trim(); + if (normalized.isEmpty()) return ""; + if (isCustomVariableToken(normalized)) return normalized; + if (isInternalVariableToken(normalized)) return INTERNAL_TOKEN_PREFIX + WiredInternalVariableSupport.normalizeKey(normalized.substring(INTERNAL_TOKEN_PREFIX.length())); + + try { + int parsed = Integer.parseInt(normalized); + return (parsed > 0) ? (CUSTOM_TOKEN_PREFIX + parsed) : ""; + } catch (NumberFormatException e) { + return ""; + } + } + + private static boolean canUseUserInternalReference(String key) { + return WiredInternalVariableSupport.canUseUserReference(key); + } + + private static boolean canUseFurniInternalReference(String key) { + return WiredInternalVariableSupport.canUseFurniReference(key); + } + + private static boolean canUseRoomInternalReference(String key) { + return WiredInternalVariableSupport.canUseRoomReference(key); + } + + private static boolean canUseContextInternalReference(String key) { + return WiredInternalVariableSupport.canUseContextReference(key); + } + + private static int param(int[] params, int index, int fallback) { + return (params != null && params.length > index) ? params[index] : fallback; + } + + private static String[] parseStringData(String value) { + return (value == null || value.isEmpty()) ? new String[0] : value.split("\\t", -1); + } + + private static int parseInteger(String value) { + try { + return (value == null || value.trim().isEmpty()) ? 0 : Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return 0; + } + } + + protected static class JsonData { + boolean selectByValue; + int comparison; + int referenceMode; + int referenceConstantValue; + int referenceTargetType; + int referenceUserSource; + int referenceFurniSource; + boolean filterExisting; + boolean invert; + String variableToken; + int variableItemId; + String referenceVariableToken; + int referenceVariableItemId; + List selectedItemIds; + int delay; + + JsonData(boolean selectByValue, int comparison, int referenceMode, int referenceConstantValue, int referenceTargetType, int referenceUserSource, int referenceFurniSource, boolean filterExisting, boolean invert, String variableToken, int variableItemId, String referenceVariableToken, int referenceVariableItemId, List selectedItemIds, int delay) { + this.selectByValue = selectByValue; + this.comparison = comparison; + this.referenceMode = referenceMode; + this.referenceConstantValue = referenceConstantValue; + this.referenceTargetType = referenceTargetType; + this.referenceUserSource = referenceUserSource; + this.referenceFurniSource = referenceFurniSource; + this.filterExisting = filterExisting; + this.invert = invert; + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.referenceVariableToken = referenceVariableToken; + this.referenceVariableItemId = referenceVariableItemId; + this.selectedItemIds = selectedItemIds; + this.delay = delay; + } + } + + private static class ReferenceSnapshot { + final int targetType; + final LinkedHashMap values = new LinkedHashMap<>(); + + ReferenceSnapshot(int targetType) { + this.targetType = targetType; + } + + void add(int entityId, int value) { + this.values.put(entityId, value); + } + + boolean isEmpty() { + return this.values.isEmpty(); + } + } + + private static class TeamEffectData { + final int colorId; + final int typeId; + + TeamEffectData(int colorId, int typeId) { + this.colorId = colorId; + this.typeId = typeId; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java index c1bfecef..9f460464 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java @@ -155,12 +155,20 @@ public class WiredTriggerAtSetTime extends InteractionWiredTrigger implements Wi // Check if enough time has passed if (this.accumulatedTime >= this.executeTime) { + if (this.getRoomId() != 0 && room.isLoaded()) { + long currentTime = System.currentTimeMillis(); + if (!WiredManager.isTriggerExecutionAllowed(room, this, currentTime)) { + return; + } + + this.hasFired = true; + this.accumulatedTime = 0; + WiredManager.triggerTimerTick(room, this); + return; + } + this.hasFired = true; this.accumulatedTime = 0; - - if (this.getRoomId() != 0 && room.isLoaded()) { - WiredManager.triggerTimerTick(room, this); - } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java index 24475dcc..8864f4c3 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java @@ -155,12 +155,20 @@ public class WiredTriggerAtTimeLong extends InteractionWiredTrigger implements W // Check if enough time has passed if (this.accumulatedTime >= this.executeTime) { + if (this.getRoomId() != 0 && room.isLoaded()) { + long currentTime = System.currentTimeMillis(); + if (!WiredManager.isTriggerExecutionAllowed(room, this, currentTime)) { + return; + } + + this.hasFired = true; + this.accumulatedTime = 0; + WiredManager.triggerTimerTick(room, this); + return; + } + this.hasFired = true; this.accumulatedTime = 0; - - if (this.getRoomId() != 0 && room.isLoaded()) { - WiredManager.triggerTimerTick(room, this); - } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerBotReachedFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerBotReachedFurni.java index 419dcb90..0bc3cee5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerBotReachedFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerBotReachedFurni.java @@ -1,6 +1,7 @@ package com.eu.habbo.habbohotel.items.interactions.wired.triggers; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; @@ -11,6 +12,8 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.WiredTriggerType; import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTriggerSourceUtil; import com.eu.habbo.messages.ServerMessage; import gnu.trove.procedure.TObjectProcedure; import gnu.trove.set.hash.THashSet; @@ -25,11 +28,15 @@ import java.util.stream.Collectors; public class WiredTriggerBotReachedFurni extends InteractionWiredTrigger { private static final Logger LOGGER = LoggerFactory.getLogger(WiredTriggerBotReachedFurni.class); + private static final int BOT_SOURCE_NAME = 100; + private static final int BOT_SOURCE_SELECTOR = 200; - public final static WiredTriggerType type = WiredTriggerType.WALKS_ON_FURNI; + public final static WiredTriggerType type = WiredTriggerType.BOT_REACHED_STF; - private THashSet items; + private final THashSet items; private String botName = ""; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + private int botSource = BOT_SOURCE_NAME; public WiredTriggerBotReachedFurni(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -72,9 +79,11 @@ public class WiredTriggerBotReachedFurni extends InteractionWiredTrigger { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(this.botName); + message.appendInt(2); + message.appendInt(this.furniSource); + message.appendInt(this.botSource); message.appendInt(0); - message.appendInt(0); - message.appendInt(WiredTriggerType.BOT_REACHED_STF.code); + message.appendInt(this.getType().code); if (!this.isTriggeredByRoomUnit()) { List invalidTriggers = new ArrayList<>(); @@ -98,9 +107,22 @@ public class WiredTriggerBotReachedFurni extends InteractionWiredTrigger { @Override public boolean saveData(WiredSettings settings) { + return this.saveData(settings, null); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { this.botName = settings.getStringParam(); + int[] params = settings.getIntParams(); + this.furniSource = (params.length > 0) + ? this.normalizeFurniSource(params[0]) + : ((settings.getFurniIds().length > 0) ? WiredSourceUtil.SOURCE_SELECTED : WiredSourceUtil.SOURCE_TRIGGER); + this.botSource = (params.length > 1) ? this.normalizeBotSource(params[1]) : BOT_SOURCE_NAME; this.items.clear(); + if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { + return true; + } int count = settings.getFurniIds().length; Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); @@ -127,20 +149,14 @@ public class WiredTriggerBotReachedFurni extends InteractionWiredTrigger { return false; } - boolean isCorrectBot = room.getBots(this.botName).stream().anyMatch(bot -> bot.getRoomUnit() == roomUnit); - if (!isCorrectBot) { + if (!this.matchesBotSource(event, roomUnit, room) || !isCorrectBotForName(roomUnit, room)) { return false; } - if (this.items.contains(sourceItem)) { - return true; - } - for (HabboItem item : room.getItemsAt(sourceItem.getX(), sourceItem.getY())) { - if (this.items.contains(item)) { - return true; - } - } - return false; + return WiredTriggerSourceUtil.containsItemOrTile( + room, + WiredTriggerSourceUtil.resolveItems(this, event, this.furniSource, this.items), + sourceItem); } @Deprecated @@ -153,6 +169,8 @@ public class WiredTriggerBotReachedFurni extends InteractionWiredTrigger { public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( this.botName, + this.furniSource, + this.botSource, this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) )); } @@ -165,12 +183,18 @@ public class WiredTriggerBotReachedFurni extends InteractionWiredTrigger { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.botName = data.botName; + this.furniSource = this.normalizeFurniSource(data.furniSource); + this.botSource = this.normalizeBotSource(data.botSource); for (Integer id: data.itemIds) { HabboItem item = room.getHabboItem(id); if (item != null) { this.items.add(item); } } + + if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } } else { String[] data = wiredData.split(":"); @@ -192,6 +216,9 @@ public class WiredTriggerBotReachedFurni extends InteractionWiredTrigger { } } } + + this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; + this.botSource = BOT_SOURCE_NAME; } } @@ -199,15 +226,51 @@ public class WiredTriggerBotReachedFurni extends InteractionWiredTrigger { public void onPickUp() { this.items.clear(); this.botName = ""; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.botSource = BOT_SOURCE_NAME; } static class JsonData { String botName; + int furniSource; + int botSource; List itemIds; - public JsonData(String botName, List itemIds) { + public JsonData(String botName, int furniSource, int botSource, List itemIds) { this.botName = botName; + this.furniSource = furniSource; + this.botSource = botSource; this.itemIds = itemIds; } } + + private boolean matchesBotSource(WiredEvent event, RoomUnit roomUnit, Room room) { + if (this.botSource == BOT_SOURCE_SELECTOR) { + return WiredTriggerSourceUtil.containsUser( + WiredTriggerSourceUtil.resolveUsers(this, event, WiredSourceUtil.SOURCE_SELECTOR, null), + roomUnit); + } + + return true; + } + + private boolean isCorrectBotForName(RoomUnit roomUnit, Room room) { + if (this.botSource != BOT_SOURCE_NAME) { + return true; + } + + return room.getBots(this.botName).stream().anyMatch(bot -> bot.getRoomUnit() == roomUnit); + } + + private int normalizeFurniSource(int value) { + if (value == WiredSourceUtil.SOURCE_SELECTED || value == WiredSourceUtil.SOURCE_SELECTOR) { + return value; + } + + return WiredSourceUtil.SOURCE_TRIGGER; + } + + private int normalizeBotSource(int value) { + return (value == BOT_SOURCE_SELECTOR) ? BOT_SOURCE_SELECTOR : BOT_SOURCE_NAME; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerBotReachedHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerBotReachedHabbo.java index 50c39e71..cc671a58 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerBotReachedHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerBotReachedHabbo.java @@ -9,6 +9,8 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.WiredTriggerType; import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTriggerSourceUtil; import com.eu.habbo.messages.ServerMessage; import java.sql.ResultSet; @@ -16,8 +18,11 @@ import java.sql.SQLException; public class WiredTriggerBotReachedHabbo extends InteractionWiredTrigger { public final static WiredTriggerType type = WiredTriggerType.BOT_REACHED_AVTR; + private static final int BOT_SOURCE_NAME = 100; + private static final int BOT_SOURCE_SELECTOR = 200; private String botName = ""; + private int botSource = BOT_SOURCE_NAME; public WiredTriggerBotReachedHabbo(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -40,7 +45,8 @@ public class WiredTriggerBotReachedHabbo extends InteractionWiredTrigger { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(this.botName); - message.appendInt(0); + message.appendInt(1); + message.appendInt(this.botSource); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -50,6 +56,7 @@ public class WiredTriggerBotReachedHabbo extends InteractionWiredTrigger { @Override public boolean saveData(WiredSettings settings) { this.botName = settings.getStringParam(); + this.botSource = (settings.getIntParams().length > 0) ? this.normalizeBotSource(settings.getIntParams()[0]) : BOT_SOURCE_NAME; return true; } @@ -58,6 +65,17 @@ public class WiredTriggerBotReachedHabbo extends InteractionWiredTrigger { public boolean matches(HabboItem triggerItem, WiredEvent event) { RoomUnit roomUnit = event.getActor().orElse(null); Room room = event.getRoom(); + + if (roomUnit == null || room == null) { + return false; + } + + if (this.botSource == BOT_SOURCE_SELECTOR) { + return WiredTriggerSourceUtil.containsUser( + WiredTriggerSourceUtil.resolveUsers(this, event, WiredSourceUtil.SOURCE_SELECTOR, null), + roomUnit); + } + return room.getBots(this.botName).stream().anyMatch(bot -> bot.getRoomUnit() == roomUnit); } @@ -70,7 +88,8 @@ public class WiredTriggerBotReachedHabbo extends InteractionWiredTrigger { @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( - this.botName + this.botName, + this.botSource )); } @@ -81,14 +100,17 @@ public class WiredTriggerBotReachedHabbo extends InteractionWiredTrigger { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.botName = data.botName; + this.botSource = this.normalizeBotSource(data.botSource); } else { this.botName = wiredData; + this.botSource = BOT_SOURCE_NAME; } } @Override public void onPickUp() { this.botName = ""; + this.botSource = BOT_SOURCE_NAME; } @Override @@ -98,9 +120,15 @@ public class WiredTriggerBotReachedHabbo extends InteractionWiredTrigger { static class JsonData { String botName; + int botSource; - public JsonData(String botName) { + public JsonData(String botName, int botSource) { this.botName = botName; + this.botSource = botSource; } } + + private int normalizeBotSource(int value) { + return (value == BOT_SOURCE_SELECTOR) ? BOT_SOURCE_SELECTOR : BOT_SOURCE_NAME; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerClockCounter.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerClockCounter.java new file mode 100644 index 00000000..dc536b3b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerClockCounter.java @@ -0,0 +1,250 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.triggers; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameUpCounter; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredTriggerType; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTriggerSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredTriggerSaveException; +import gnu.trove.set.hash.THashSet; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.stream.Collectors; + +public class WiredTriggerClockCounter extends InteractionWiredTrigger { + private static final int MAX_MINUTES = 99; + private static final int MAX_HALF_SECOND_STEPS = 119; + + public static final WiredTriggerType type = WiredTriggerType.CLOCK_COUNTER; + + private final THashSet items; + private int minutes = 0; + private int halfSecondSteps = 0; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredTriggerClockCounter(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + this.items = new THashSet<>(); + } + + public WiredTriggerClockCounter(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + this.items = new THashSet<>(); + } + + @Override + public boolean matches(HabboItem triggerItem, WiredEvent event) { + HabboItem sourceItem = event.getSourceItem().orElse(null); + + if (!(sourceItem instanceof InteractionGameUpCounter)) { + return false; + } + + if (((InteractionGameUpCounter) sourceItem).getCurrentTimeInMs() != this.getTargetTimeInMs()) { + return false; + } + + return WiredTriggerSourceUtil.resolveItems(this, event, this.furniSource, this.items).stream() + .anyMatch(item -> item != null && item.getId() == sourceItem.getId()); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public WiredTriggerType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + this.refresh(room); + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(this.items.size()); + + for (HabboItem item : this.items) { + message.appendInt(item.getId()); + } + + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(3); + message.appendInt(this.minutes); + message.appendInt(this.halfSecondSteps); + message.appendInt(this.furniSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + return this.saveData(settings, null); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + int[] params = settings.getIntParams(); + + this.minutes = (params.length > 0) ? this.normalizeMinutes(params[0]) : 0; + this.halfSecondSteps = (params.length > 1) ? this.normalizeHalfSecondSteps(params[1]) : 0; + this.furniSource = (params.length > 2) + ? this.normalizeFurniSource(params[2]) + : ((settings.getFurniIds().length > 0) ? WiredSourceUtil.SOURCE_SELECTED : WiredSourceUtil.SOURCE_TRIGGER); + + this.items.clear(); + + if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { + return true; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + return false; + } + + int count = settings.getFurniIds().length; + if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) { + return false; + } + + for (int itemId : settings.getFurniIds()) { + HabboItem item = room.getHabboItem(itemId); + + if (!(item instanceof InteractionGameUpCounter)) { + throw new WiredTriggerSaveException("wiredfurni.error.require_counter_furni"); + } + + this.items.add(item); + } + + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.minutes, + this.halfSecondSteps, + this.furniSource, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.items.clear(); + this.minutes = 0; + this.halfSecondSteps = 0; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (!wiredData.startsWith("{")) { + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.minutes = this.normalizeMinutes(data.minutes); + this.halfSecondSteps = this.normalizeHalfSecondSteps(data.halfSecondSteps); + this.furniSource = this.normalizeFurniSource(data.furniSource); + + if (data.itemIds == null) { + return; + } + + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + if (item instanceof InteractionGameUpCounter) { + this.items.add(item); + } + } + + if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + } + } + + @Override + public void onPickUp() { + this.items.clear(); + this.minutes = 0; + this.halfSecondSteps = 0; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + } + + private void refresh(Room room) { + THashSet remove = new THashSet<>(); + + for (HabboItem item : this.items) { + HabboItem roomItem = room.getHabboItem(item.getId()); + if (!(roomItem instanceof InteractionGameUpCounter)) { + remove.add(item); + } + } + + for (HabboItem item : remove) { + this.items.remove(item); + } + } + + private int getTargetTimeInMs() { + return (this.minutes * 60_000) + (this.halfSecondSteps * 500); + } + + private int normalizeMinutes(int value) { + return Math.max(0, Math.min(MAX_MINUTES, value)); + } + + private int normalizeHalfSecondSteps(int value) { + return Math.max(0, Math.min(MAX_HALF_SECOND_STEPS, value)); + } + + private int normalizeFurniSource(int value) { + if (value == WiredSourceUtil.SOURCE_SELECTED || value == WiredSourceUtil.SOURCE_SELECTOR) { + return value; + } + + return WiredSourceUtil.SOURCE_TRIGGER; + } + + static class JsonData { + int minutes; + int halfSecondSteps; + int furniSource; + List itemIds; + + public JsonData(int minutes, int halfSecondSteps, int furniSource, List itemIds) { + this.minutes = minutes; + this.halfSecondSteps = halfSecondSteps; + this.furniSource = furniSource; + this.itemIds = itemIds; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerFurniStateToggled.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerFurniStateToggled.java index 4f4a82f4..0ac5b376 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerFurniStateToggled.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerFurniStateToggled.java @@ -10,41 +10,59 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.WiredTriggerType; import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTriggerSourceUtil; import com.eu.habbo.messages.ServerMessage; import gnu.trove.set.hash.THashSet; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; public class WiredTriggerFurniStateToggled extends InteractionWiredTrigger { private static final WiredTriggerType type = WiredTriggerType.STATE_CHANGED; + private static final int MODE_ALL_STATES = 0; + private static final int MODE_SAVED_STATE = 1; - private THashSet items; + private THashSet snapshots; + private int triggerMode = MODE_ALL_STATES; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; public WiredTriggerFurniStateToggled(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); - this.items = new THashSet<>(); + this.snapshots = new THashSet<>(); } public WiredTriggerFurniStateToggled(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { super(id, userId, item, extradata, limitedStack, limitedSells); - this.items = new THashSet<>(); + this.snapshots = new THashSet<>(); } @Override public boolean matches(HabboItem triggerItem, WiredEvent event) { - // Reject if this was triggered by a wired effect (to prevent loops) if (event.isTriggeredByEffect()) { return false; } HabboItem sourceItem = event.getSourceItem().orElse(null); - if (sourceItem != null) { - return this.items.contains(sourceItem); + if (sourceItem == null) { + return false; } - return false; + + StateSnapshot snapshot = this.getSnapshot(sourceItem.getId()); + if (!this.matchesSourceItem(event, sourceItem)) { + return false; + } + + if (this.triggerMode == MODE_SAVED_STATE) { + if (snapshot == null) { + return false; + } + return snapshot.state.equals(this.normalizeState(sourceItem.getExtradata())); + } + + return true; } @Deprecated @@ -56,22 +74,44 @@ public class WiredTriggerFurniStateToggled extends InteractionWiredTrigger { @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( - this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) + this.triggerMode, + this.furniSource, + new ArrayList<>(this.snapshots) )); } @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { - this.items = new THashSet<>(); + this.snapshots = new THashSet<>(); + this.triggerMode = MODE_ALL_STATES; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; String wiredData = set.getString("wired_data"); - if (wiredData.startsWith("{")) { + if (wiredData != null && wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - for (Integer id: data.itemIds) { - HabboItem item = room.getHabboItem(id); - if (item != null) { - this.items.add(item); + this.triggerMode = (data != null) ? data.triggerMode : MODE_ALL_STATES; + this.furniSource = (data != null) ? this.normalizeFurniSource(data.furniSource) : WiredSourceUtil.SOURCE_TRIGGER; + + if (data != null && data.snapshots != null && !data.snapshots.isEmpty()) { + for (StateSnapshot snapshot : data.snapshots) { + if (snapshot == null) continue; + + HabboItem item = room.getHabboItem(snapshot.itemId); + if (item != null) { + this.snapshots.add(new StateSnapshot(item.getId(), snapshot.state)); + } } + } else if (data != null && data.itemIds != null) { + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + if (item != null) { + this.snapshots.add(this.captureSnapshot(item)); + } + } + } + + if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.snapshots.isEmpty()) { + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; } } else { if (wiredData.split(":").length >= 3) { @@ -79,19 +119,28 @@ public class WiredTriggerFurniStateToggled extends InteractionWiredTrigger { if (!wiredData.split(":")[2].equals("\t")) { for (String s : wiredData.split(":")[2].split(";")) { + if (s.isEmpty()) { + continue; + } + HabboItem item = room.getHabboItem(Integer.parseInt(s)); - if (item != null) - this.items.add(item); + if (item != null) { + this.snapshots.add(this.captureSnapshot(item)); + } } } } + + this.furniSource = this.snapshots.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; } } @Override public void onPickUp() { - this.items.clear(); + this.snapshots.clear(); + this.triggerMode = MODE_ALL_STATES; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; } @Override @@ -101,33 +150,32 @@ public class WiredTriggerFurniStateToggled extends InteractionWiredTrigger { @Override public void serializeWiredData(ServerMessage message, Room room) { - THashSet items = new THashSet<>(); + THashSet snapshotsToRemove = new THashSet<>(); - for (HabboItem item : this.items) { - if (item.getRoomId() != this.getRoomId()) { - items.add(item); + for (StateSnapshot snapshot : this.snapshots) { + HabboItem item = room.getHabboItem(snapshot.itemId); + if (item == null || item.getRoomId() != this.getRoomId()) { + snapshotsToRemove.add(snapshot); continue; } - - if (room.getHabboItem(item.getId()) == null) { - items.add(item); - } } - for (HabboItem item : items) { - this.items.remove(item); + for (StateSnapshot snapshot : snapshotsToRemove) { + this.snapshots.remove(snapshot); } message.appendBoolean(false); message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); - message.appendInt(this.items.size()); - for (HabboItem item : this.items) { - message.appendInt(item.getId()); + message.appendInt(this.snapshots.size()); + for (StateSnapshot snapshot : this.snapshots) { + message.appendInt(snapshot.itemId); } message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(0); + message.appendInt(2); + message.appendInt(this.triggerMode); + message.appendInt(this.furniSource); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -135,14 +183,25 @@ public class WiredTriggerFurniStateToggled extends InteractionWiredTrigger { @Override public boolean saveData(WiredSettings settings) { - this.items.clear(); + this.snapshots.clear(); + this.triggerMode = (settings.getIntParams().length > 0 && settings.getIntParams()[0] == MODE_SAVED_STATE) + ? MODE_SAVED_STATE + : MODE_ALL_STATES; + this.furniSource = (settings.getIntParams().length > 1) + ? this.normalizeFurniSource(settings.getIntParams()[1]) + : ((settings.getFurniIds().length > 0) ? WiredSourceUtil.SOURCE_SELECTED : WiredSourceUtil.SOURCE_TRIGGER); + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + return true; + } int count = settings.getFurniIds().length; for (int i = 0; i < count; i++) { - HabboItem item = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()).getHabboItem(settings.getFurniIds()[i]); + HabboItem item = room.getHabboItem(settings.getFurniIds()[i]); if (item != null) { - this.items.add(item); + this.snapshots.add(this.captureSnapshot(item)); } } @@ -154,11 +213,102 @@ public class WiredTriggerFurniStateToggled extends InteractionWiredTrigger { return true; } + private StateSnapshot captureSnapshot(HabboItem item) { + return new StateSnapshot(item.getId(), this.normalizeState(item.getExtradata())); + } + + private StateSnapshot getSnapshot(int itemId) { + for (StateSnapshot snapshot : this.snapshots) { + if (snapshot.itemId == itemId) { + return snapshot; + } + } + + return null; + } + + private String normalizeState(String state) { + return (state == null) ? "" : state; + } + + private boolean matchesSourceItem(WiredEvent event, HabboItem sourceItem) { + List selectedItems = new ArrayList<>(); + + if (event.getRoom() != null) { + for (StateSnapshot snapshot : this.snapshots) { + HabboItem item = event.getRoom().getHabboItem(snapshot.itemId); + if (item != null) { + selectedItems.add(item); + } + } + } + + for (HabboItem item : WiredTriggerSourceUtil.resolveItems(this, event, this.furniSource, selectedItems)) { + if (item != null && item.getId() == sourceItem.getId()) { + return true; + } + } + + return false; + } + + private int normalizeFurniSource(int value) { + if (value == WiredSourceUtil.SOURCE_SELECTED || value == WiredSourceUtil.SOURCE_SELECTOR) { + return value; + } + + return WiredSourceUtil.SOURCE_TRIGGER; + } + static class JsonData { + int triggerMode; + int furniSource; + List snapshots; List itemIds; + public JsonData() { + } + public JsonData(List itemIds) { this.itemIds = itemIds; } + + public JsonData(int triggerMode, int furniSource, List snapshots) { + this.triggerMode = triggerMode; + this.furniSource = furniSource; + this.snapshots = snapshots; + } + } + + static class StateSnapshot { + int itemId; + String state; + + public StateSnapshot() { + } + + public StateSnapshot(int itemId, String state) { + this.itemId = itemId; + this.state = (state == null) ? "" : state; + } + + @Override + public int hashCode() { + return Integer.hashCode(this.itemId); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof StateSnapshot)) { + return false; + } + + StateSnapshot that = (StateSnapshot) object; + return this.itemId == that.itemId; + } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboClicksFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboClicksFurni.java new file mode 100644 index 00000000..6b48577e --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboClicksFurni.java @@ -0,0 +1,243 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.triggers; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredTriggerType; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTriggerSourceUtil; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredTriggerSaveException; +import gnu.trove.set.hash.THashSet; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.stream.Collectors; + +public class WiredTriggerHabboClicksFurni extends InteractionWiredTrigger { + public static final WiredTriggerType type = WiredTriggerType.CLICKS_FURNI; + + protected final THashSet items; + protected int furniSource = WiredSourceUtil.SOURCE_TRIGGER; + + public WiredTriggerHabboClicksFurni(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + this.items = new THashSet<>(); + } + + public WiredTriggerHabboClicksFurni(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + this.items = new THashSet<>(); + } + + @Override + public boolean matches(HabboItem triggerItem, WiredEvent event) { + HabboItem sourceItem = event.getSourceItem().orElse(null); + if (sourceItem == null) { + return false; + } + + return this.matchesSourceItem(this.resolveCandidateItems(triggerItem, event), sourceItem); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public WiredTriggerType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + THashSet items = new THashSet<>(); + + if (Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()) == null) { + items.addAll(this.items); + } else { + for (HabboItem item : this.items) { + if (Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()).getHabboItem(item.getId()) == null) { + items.add(item); + } + } + } + + for (HabboItem item : items) { + this.items.remove(item); + } + + message.appendBoolean(false); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(this.items.size()); + for (HabboItem item : this.items) { + message.appendInt(item.getId()); + } + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(1); + message.appendInt(this.furniSource); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + return this.saveData(settings, null); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + this.items.clear(); + this.furniSource = (settings.getIntParams().length > 0) + ? this.normalizeFurniSource(settings.getIntParams()[0]) + : ((settings.getFurniIds().length > 0) ? WiredSourceUtil.SOURCE_SELECTED : WiredSourceUtil.SOURCE_TRIGGER); + + if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { + return true; + } + + int count = settings.getFurniIds().length; + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + return false; + } + + for (int i = 0; i < count; i++) { + HabboItem item = room.getHabboItem(settings.getFurniIds()[i]); + if (item != null) { + if (!this.isSelectableItem(item)) { + throw new WiredTriggerSaveException(this.getInvalidSelectionErrorKey()); + } + this.items.add(item); + } + } + + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.furniSource, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.items.clear(); + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + String wiredData = set.getString("wired_data"); + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.furniSource = this.normalizeFurniSource(data.furniSource); + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + if (item != null) { + this.items.add(item); + } + } + } else { + if (wiredData.split(":").length >= 3) { + super.setDelay(Integer.parseInt(wiredData.split(":")[0])); + + if (!wiredData.split(":")[2].equals("\t")) { + for (String s : wiredData.split(":")[2].split(";")) { + if (s.isEmpty()) { + continue; + } + + try { + HabboItem item = room.getHabboItem(Integer.parseInt(s)); + + if (item != null) { + this.items.add(item); + } + } catch (Exception e) { + } + } + } + } + + this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; + } + } + + @Override + public void onPickUp() { + this.items.clear(); + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + } + + @Override + public boolean isTriggeredByRoomUnit() { + return true; + } + + static class JsonData { + int furniSource; + List itemIds; + + public JsonData(int furniSource, List itemIds) { + this.furniSource = furniSource; + this.itemIds = itemIds; + } + } + + protected boolean isSelectableItem(HabboItem item) { + return item != null; + } + + protected String getInvalidSelectionErrorKey() { + return "There was an error while saving that trigger"; + } + + protected int normalizeFurniSource(int value) { + if (value == WiredSourceUtil.SOURCE_SELECTED || value == WiredSourceUtil.SOURCE_SELECTOR) { + return value; + } + + return WiredSourceUtil.SOURCE_TRIGGER; + } + + private Iterable resolveCandidateItems(HabboItem triggerItem, WiredEvent event) { + switch (this.furniSource) { + case WiredSourceUtil.SOURCE_SELECTED: + return this.items; + case WiredSourceUtil.SOURCE_SELECTOR: + return WiredTriggerSourceUtil.resolveItems(this, event, WiredSourceUtil.SOURCE_SELECTOR, this.items); + case WiredSourceUtil.SOURCE_TRIGGER: + default: + return (triggerItem != null) ? java.util.Collections.singletonList(triggerItem) : java.util.Collections.emptyList(); + } + } + + private boolean matchesSourceItem(Iterable candidateItems, HabboItem sourceItem) { + if (candidateItems == null || sourceItem == null) { + return false; + } + + for (HabboItem item : candidateItems) { + if (item != null && item.getId() == sourceItem.getId()) { + return true; + } + } + + return false; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboClicksTile.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboClicksTile.java new file mode 100644 index 00000000..118c265e --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboClicksTile.java @@ -0,0 +1,57 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.triggers; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredTriggerType; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredTriggerHabboClicksTile extends WiredTriggerHabboClicksFurni { + public static final WiredTriggerType type = WiredTriggerType.CLICKS_TILE; + + private static final String CLICK_TILE_INTERACTION = "room_invisible_click_tile"; + + public WiredTriggerHabboClicksTile(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredTriggerHabboClicksTile(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean matches(HabboItem triggerItem, WiredEvent event) { + if (!super.matches(triggerItem, event)) { + return false; + } + + HabboItem sourceItem = event.getSourceItem().orElse(null); + return isClickTileItem(sourceItem); + } + + @Override + public WiredTriggerType getType() { + return type; + } + + private boolean isClickTileItem(HabboItem item) { + if (item == null || item.getBaseItem() == null || item.getBaseItem().getInteractionType() == null) { + return false; + } + + String interaction = item.getBaseItem().getInteractionType().getName(); + return interaction != null && interaction.equalsIgnoreCase(CLICK_TILE_INTERACTION); + } + + @Override + protected boolean isSelectableItem(HabboItem item) { + return this.isClickTileItem(item); + } + + @Override + protected String getInvalidSelectionErrorKey() { + return "wiredfurni.error.require_click_tiles"; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboClicksUser.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboClicksUser.java new file mode 100644 index 00000000..53a42939 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboClicksUser.java @@ -0,0 +1,187 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.triggers; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredTriggerType; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredTriggerHabboClicksUser extends InteractionWiredTrigger { + public static final WiredTriggerType type = WiredTriggerType.CLICKS_USER; + private static final String CACHE_BLOCK_MENU_OPEN = "wired.click_user.block_menu_open"; + private static final String CACHE_IGNORE_LOOK_UNTIL = "wired.click_user.ignore_look_until"; + private static final long IGNORE_LOOK_WINDOW_MS = 500L; + private boolean blockMenuOpen = false; + private boolean doNotRotate = false; + + public WiredTriggerHabboClicksUser(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredTriggerHabboClicksUser(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean matches(HabboItem triggerItem, WiredEvent event) { + return event.getActor().isPresent(); + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData(this.blockMenuOpen, this.doNotRotate)); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + String wiredData = set.getString("wired_data"); + + this.blockMenuOpen = false; + this.doNotRotate = false; + + if (wiredData != null && wiredData.startsWith("{")) { + JsonData data = com.eu.habbo.habbohotel.wired.core.WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data != null) { + this.blockMenuOpen = data.blockMenuOpen; + this.doNotRotate = data.doNotRotate; + } + } + } + + @Override + public void onPickUp() { + this.blockMenuOpen = false; + this.doNotRotate = false; + } + + @Override + public WiredTriggerType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(2); + message.appendInt(this.blockMenuOpen ? 1 : 0); + message.appendInt(this.doNotRotate ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] params = settings.getIntParams(); + this.blockMenuOpen = (params.length > 0) && (params[0] == 1); + this.doNotRotate = (params.length > 1) && (params[1] == 1); + return true; + } + + @Override + public boolean isTriggeredByRoomUnit() { + return true; + } + + public boolean isBlockMenuOpen() { + return this.blockMenuOpen; + } + + public boolean isDoNotRotate() { + return this.doNotRotate; + } + + public static void clearRuntimeFlags(RoomUnit roomUnit) { + if (roomUnit == null) { + return; + } + + roomUnit.getCacheable().remove(CACHE_BLOCK_MENU_OPEN); + roomUnit.getCacheable().remove(CACHE_IGNORE_LOOK_UNTIL); + } + + public static void applyRuntimeOptions(RoomUnit roomUnit, boolean blockMenuOpen, boolean doNotRotate) { + if (roomUnit == null) { + return; + } + + if (blockMenuOpen) { + roomUnit.getCacheable().put(CACHE_BLOCK_MENU_OPEN, Boolean.TRUE); + } + + if (doNotRotate) { + roomUnit.getCacheable().put(CACHE_IGNORE_LOOK_UNTIL, System.currentTimeMillis() + IGNORE_LOOK_WINDOW_MS); + } + } + + public static boolean consumeBlockMenuOpen(RoomUnit roomUnit) { + if (roomUnit == null) { + return false; + } + + Object value = roomUnit.getCacheable().remove(CACHE_BLOCK_MENU_OPEN); + return Boolean.TRUE.equals(value); + } + + public static boolean consumeIgnoreLook(RoomUnit roomUnit) { + if (roomUnit == null) { + return false; + } + + Object value = roomUnit.getCacheable().get(CACHE_IGNORE_LOOK_UNTIL); + + if (!(value instanceof Long)) { + roomUnit.getCacheable().remove(CACHE_IGNORE_LOOK_UNTIL); + return false; + } + + long expiresAt = (Long) value; + roomUnit.getCacheable().remove(CACHE_IGNORE_LOOK_UNTIL); + + return System.currentTimeMillis() <= expiresAt; + } + + public static boolean hasPendingIgnoreLook(RoomUnit roomUnit) { + if (roomUnit == null) { + return false; + } + + Object value = roomUnit.getCacheable().get(CACHE_IGNORE_LOOK_UNTIL); + + if (!(value instanceof Long)) { + return false; + } + + return System.currentTimeMillis() <= (Long) value; + } + + static class JsonData { + boolean blockMenuOpen; + boolean doNotRotate; + + public JsonData(boolean blockMenuOpen, boolean doNotRotate) { + this.blockMenuOpen = blockMenuOpen; + this.doNotRotate = doNotRotate; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboLeavesRoom.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboLeavesRoom.java new file mode 100644 index 00000000..36583ce3 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboLeavesRoom.java @@ -0,0 +1,116 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.triggers; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +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.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.WiredTriggerType; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredTriggerHabboLeavesRoom extends InteractionWiredTrigger { + public static final WiredTriggerType type = WiredTriggerType.LEAVE_ROOM; + + private String username = ""; + + public WiredTriggerHabboLeavesRoom(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredTriggerHabboLeavesRoom(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean matches(HabboItem triggerItem, WiredEvent event) { + RoomUnit roomUnit = event.getActor().orElse(null); + Room room = event.getRoom(); + Habbo habbo = room.getHabbo(roomUnit); + + if (habbo != null) { + if (this.username.length() > 0) { + return habbo.getHabboInfo().getUsername().equalsIgnoreCase(this.username); + } + + return true; + } + return false; + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.username + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + String wiredData = set.getString("wired_data"); + + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.username = data.username; + } else { + this.username = wiredData; + } + } + + @Override + public void onPickUp() { + this.username = ""; + } + + @Override + public WiredTriggerType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.username); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + this.username = settings.getStringParam(); + + return true; + } + + @Override + public boolean isTriggeredByRoomUnit() { + return true; + } + + static class JsonData { + String username; + + public JsonData(String username) { + this.username = username; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboPerformsAction.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboPerformsAction.java new file mode 100644 index 00000000..07b2195d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboPerformsAction.java @@ -0,0 +1,217 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.triggers; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredTriggerType; +import com.eu.habbo.habbohotel.wired.WiredUserActionType; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredTriggerHabboPerformsAction extends InteractionWiredTrigger { + private static final WiredTriggerType type = WiredTriggerType.USER_PERFORMS_ACTION; + private static final int DEFAULT_ACTION = WiredUserActionType.WAVE; + + private int selectedAction = DEFAULT_ACTION; + private boolean signFilterEnabled = false; + private int signId = 0; + private boolean danceFilterEnabled = false; + private int danceId = 1; + + public WiredTriggerHabboPerformsAction(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredTriggerHabboPerformsAction(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean matches(HabboItem triggerItem, WiredEvent event) { + if (!event.getActor().isPresent()) { + return false; + } + + if (event.getActionId() != this.selectedAction) { + return false; + } + + if (this.selectedAction == WiredUserActionType.SIGN && this.signFilterEnabled) { + return event.getActionParameter() == this.signId; + } + + if (this.selectedAction == WiredUserActionType.DANCE && this.danceFilterEnabled) { + return event.getActionParameter() == this.danceId; + } + + return true; + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.selectedAction, + this.signFilterEnabled, + this.signId, + this.danceFilterEnabled, + this.danceId + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.resetSettings(); + + String wiredData = set.getString("wired_data"); + + if (wiredData != null && wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + + if (data == null) { + return; + } + + this.selectedAction = normalizeAction(data.selectedAction); + this.signFilterEnabled = data.signFilterEnabled; + this.signId = normalizeSignId(data.signId); + this.danceFilterEnabled = data.danceFilterEnabled; + this.danceId = normalizeDanceId(data.danceId); + } + } + + @Override + public void onPickUp() { + this.resetSettings(); + } + + @Override + public WiredTriggerType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(5); + message.appendInt(this.selectedAction); + message.appendInt(this.signFilterEnabled ? 1 : 0); + message.appendInt(this.signId); + message.appendInt(this.danceFilterEnabled ? 1 : 0); + message.appendInt(this.danceId); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + int[] intParams = settings.getIntParams(); + + this.resetSettings(); + + if (intParams.length > 0) { + this.selectedAction = normalizeAction(intParams[0]); + } + + if (intParams.length > 1) { + this.signFilterEnabled = (intParams[1] == 1); + } + + if (intParams.length > 2) { + this.signId = normalizeSignId(intParams[2]); + } + + if (intParams.length > 3) { + this.danceFilterEnabled = (intParams[3] == 1); + } + + if (intParams.length > 4) { + this.danceId = normalizeDanceId(intParams[4]); + } + + return true; + } + + @Override + public boolean isTriggeredByRoomUnit() { + return true; + } + + private void resetSettings() { + this.selectedAction = DEFAULT_ACTION; + this.signFilterEnabled = false; + this.signId = 0; + this.danceFilterEnabled = false; + this.danceId = 1; + } + + private int normalizeAction(int action) { + switch (action) { + case WiredUserActionType.WAVE: + case WiredUserActionType.BLOW_KISS: + case WiredUserActionType.LAUGH: + case WiredUserActionType.AWAKE: + case WiredUserActionType.RELAX: + case WiredUserActionType.SIT: + case WiredUserActionType.STAND: + case WiredUserActionType.LAY: + case WiredUserActionType.SIGN: + case WiredUserActionType.DANCE: + case WiredUserActionType.THUMB_UP: + return action; + default: + return DEFAULT_ACTION; + } + } + + private int normalizeSignId(int signId) { + if (signId < 0 || signId > 17) { + return 0; + } + + return signId; + } + + private int normalizeDanceId(int danceId) { + if (danceId < 1 || danceId > 4) { + return 1; + } + + return danceId; + } + + static class JsonData { + int selectedAction; + boolean signFilterEnabled; + int signId; + boolean danceFilterEnabled; + int danceId; + + public JsonData(int selectedAction, boolean signFilterEnabled, int signId, boolean danceFilterEnabled, int danceId) { + this.selectedAction = selectedAction; + this.signFilterEnabled = signFilterEnabled; + this.signId = signId; + this.danceFilterEnabled = danceFilterEnabled; + this.danceId = danceId; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboSaysKeyword.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboSaysKeyword.java index b05746eb..26d43d33 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboSaysKeyword.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboSaysKeyword.java @@ -17,9 +17,14 @@ import java.sql.SQLException; public class WiredTriggerHabboSaysKeyword extends InteractionWiredTrigger { private static final WiredTriggerType type = WiredTriggerType.SAY_SOMETHING; + private static final int MATCH_CONTAINS = 0; + private static final int MATCH_EXACT = 1; + private static final int MATCH_ALL_WORDS = 2; + private boolean hideMessage = false; private boolean ownerOnly = false; private String key = ""; + private int matchMode = MATCH_CONTAINS; public WiredTriggerHabboSaysKeyword(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -31,16 +36,20 @@ public class WiredTriggerHabboSaysKeyword extends InteractionWiredTrigger { @Override public boolean matches(HabboItem triggerItem, WiredEvent event) { - if (this.key.length() > 0) { - String text = event.getText().orElse(null); - if (text != null && text.toLowerCase().contains(this.key.toLowerCase())) { - RoomUnit roomUnit = event.getActor().orElse(null); - Room room = event.getRoom(); - Habbo habbo = room.getHabbo(roomUnit); - return !this.ownerOnly || (habbo != null && room.getOwnerId() == habbo.getHabboInfo().getId()); - } + if ((this.matchMode != MATCH_ALL_WORDS) && this.key.length() <= 0) { + return false; } - return false; + + String text = event.getText().orElse(null); + RoomUnit roomUnit = event.getActor().orElse(null); + Room room = event.getRoom(); + + if (text == null || roomUnit == null || room == null || !this.matchesText(text)) { + return false; + } + + Habbo habbo = room.getHabbo(roomUnit); + return !this.ownerOnly || (habbo != null && room.getOwnerId() == habbo.getHabboInfo().getId()); } @Deprecated @@ -52,8 +61,10 @@ public class WiredTriggerHabboSaysKeyword extends InteractionWiredTrigger { @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( + this.hideMessage, this.ownerOnly, - this.key + this.key, + this.matchMode )); } @@ -64,21 +75,27 @@ public class WiredTriggerHabboSaysKeyword extends InteractionWiredTrigger { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.ownerOnly = data.ownerOnly; + this.hideMessage = data.hideMessage; this.key = data.key; + this.matchMode = this.normalizeMatchMode(data.matchMode); } else { String[] data = wiredData.split("\t"); if (data.length == 2) { this.ownerOnly = data[0].equalsIgnoreCase("1"); this.key = data[1]; + this.hideMessage = false; + this.matchMode = MATCH_CONTAINS; } } } @Override public void onPickUp() { + this.hideMessage = false; this.ownerOnly = false; this.key = ""; + this.matchMode = MATCH_CONTAINS; } @Override @@ -94,8 +111,11 @@ public class WiredTriggerHabboSaysKeyword extends InteractionWiredTrigger { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(this.key); + message.appendInt(3); + message.appendInt(this.matchMode); + message.appendInt(this.hideMessage ? 1 : 0); + message.appendInt(this.ownerOnly ? 1 : 0); message.appendInt(0); - message.appendInt(1); message.appendInt(this.getType().code); message.appendInt(0); message.appendInt(0); @@ -103,9 +123,11 @@ public class WiredTriggerHabboSaysKeyword extends InteractionWiredTrigger { @Override public boolean saveData(WiredSettings settings) { - if(settings.getIntParams().length < 1) return false; - this.ownerOnly = settings.getIntParams()[0] == 1; - this.key = settings.getStringParam(); + int[] params = settings.getIntParams(); + this.matchMode = (params.length > 0) ? this.normalizeMatchMode(params[0]) : MATCH_CONTAINS; + this.hideMessage = (params.length > 1) && (params[1] == 1); + this.ownerOnly = (params.length > 2) && (params[2] == 1); + this.key = (this.matchMode == MATCH_ALL_WORDS) ? "" : settings.getStringParam(); return true; } @@ -115,13 +137,56 @@ public class WiredTriggerHabboSaysKeyword extends InteractionWiredTrigger { return true; } + public boolean isHideMessage() { + return this.hideMessage; + } + + public boolean isOwnerOnly() { + return this.ownerOnly; + } + + public String getKey() { + return this.key; + } + + public int getMatchMode() { + return this.matchMode; + } + + private boolean matchesText(String text) { + String normalizedText = text.toLowerCase().trim(); + String normalizedKey = this.key.toLowerCase().trim(); + + switch (this.matchMode) { + case MATCH_EXACT: + return normalizedText.equals(normalizedKey); + case MATCH_ALL_WORDS: + return !normalizedText.isEmpty(); + case MATCH_CONTAINS: + default: + return normalizedText.contains(normalizedKey); + } + } + + private int normalizeMatchMode(int value) { + if (value < MATCH_CONTAINS || value > MATCH_ALL_WORDS) { + return MATCH_CONTAINS; + } + + return value; + } + static class JsonData { + boolean hideMessage; boolean ownerOnly; String key; + int matchMode; - public JsonData(boolean ownerOnly, String key) { + public JsonData(boolean hideMessage, boolean ownerOnly, String key, int matchMode) { + this.hideMessage = hideMessage; this.ownerOnly = ownerOnly; this.key = key; + this.matchMode = matchMode; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboWalkOffFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboWalkOffFurni.java index 3e83d923..d08790a8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboWalkOffFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboWalkOffFurni.java @@ -1,6 +1,7 @@ package com.eu.habbo.habbohotel.items.interactions.wired.triggers; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; @@ -10,6 +11,8 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.WiredTriggerType; import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTriggerSourceUtil; import com.eu.habbo.messages.ServerMessage; import gnu.trove.set.hash.THashSet; @@ -21,7 +24,8 @@ import java.util.stream.Collectors; public class WiredTriggerHabboWalkOffFurni extends InteractionWiredTrigger { public static final WiredTriggerType type = WiredTriggerType.WALKS_OFF_FURNI; - private THashSet items; + private final THashSet items; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; public WiredTriggerHabboWalkOffFurni(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -36,24 +40,11 @@ public class WiredTriggerHabboWalkOffFurni extends InteractionWiredTrigger { @Override public boolean matches(HabboItem triggerItem, WiredEvent event) { HabboItem sourceItem = event.getSourceItem().orElse(null); - if (sourceItem == null) { - return false; - } - - if (this.items.contains(sourceItem)) { - return true; - } - Room room = event.getRoom(); - if (room != null) { - for (HabboItem item : room.getItemsAt(sourceItem.getX(), sourceItem.getY())) { - if (this.items.contains(item)) { - return true; - } - } - } - - return false; + return WiredTriggerSourceUtil.containsItemOrTile( + room, + WiredTriggerSourceUtil.resolveItems(this, event, this.furniSource, this.items), + sourceItem); } @Deprecated @@ -64,7 +55,8 @@ public class WiredTriggerHabboWalkOffFurni extends InteractionWiredTrigger { @Override public String getWiredData() { - return WiredManager.getGson().toJson(new WiredTriggerFurniStateToggled.JsonData( + return WiredManager.getGson().toJson(new JsonData( + this.furniSource, this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) )); } @@ -72,10 +64,12 @@ public class WiredTriggerHabboWalkOffFurni extends InteractionWiredTrigger { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { this.items.clear(); + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; String wiredData = set.getString("wired_data"); if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.furniSource = this.normalizeFurniSource(data.furniSource); for (Integer id: data.itemIds) { HabboItem item = room.getHabboItem(id); if (item != null) { @@ -101,12 +95,15 @@ public class WiredTriggerHabboWalkOffFurni extends InteractionWiredTrigger { } } } + + this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; } } @Override public void onPickUp() { this.items.clear(); + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; } @Override @@ -140,7 +137,8 @@ public class WiredTriggerHabboWalkOffFurni extends InteractionWiredTrigger { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(0); + message.appendInt(1); + message.appendInt(this.furniSource); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -149,12 +147,28 @@ public class WiredTriggerHabboWalkOffFurni extends InteractionWiredTrigger { @Override public boolean saveData(WiredSettings settings) { + return this.saveData(settings, null); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { this.items.clear(); + this.furniSource = (settings.getIntParams().length > 0) + ? this.normalizeFurniSource(settings.getIntParams()[0]) + : ((settings.getFurniIds().length > 0) ? WiredSourceUtil.SOURCE_SELECTED : WiredSourceUtil.SOURCE_TRIGGER); + + if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { + return true; + } int count = settings.getFurniIds().length; + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + return false; + } for (int i = 0; i < count; i++) { - HabboItem item = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()).getHabboItem(settings.getFurniIds()[i]); + HabboItem item = room.getHabboItem(settings.getFurniIds()[i]); if (item != null) { this.items.add(item); } @@ -169,10 +183,20 @@ public class WiredTriggerHabboWalkOffFurni extends InteractionWiredTrigger { } static class JsonData { + int furniSource; List itemIds; - public JsonData(List itemIds) { + public JsonData(int furniSource, List itemIds) { + this.furniSource = furniSource; this.itemIds = itemIds; } } + + private int normalizeFurniSource(int value) { + if (value == WiredSourceUtil.SOURCE_SELECTED || value == WiredSourceUtil.SOURCE_SELECTOR) { + return value; + } + + return WiredSourceUtil.SOURCE_TRIGGER; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboWalkOnFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboWalkOnFurni.java index 6f8d7ddc..d6fb8f96 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboWalkOnFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerHabboWalkOnFurni.java @@ -1,6 +1,7 @@ package com.eu.habbo.habbohotel.items.interactions.wired.triggers; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; @@ -10,6 +11,8 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.WiredTriggerType; import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTriggerSourceUtil; import com.eu.habbo.messages.ServerMessage; import gnu.trove.set.hash.THashSet; @@ -21,7 +24,8 @@ import java.util.stream.Collectors; public class WiredTriggerHabboWalkOnFurni extends InteractionWiredTrigger { public static final WiredTriggerType type = WiredTriggerType.WALKS_ON_FURNI; - private THashSet items; + private final THashSet items; + private int furniSource = WiredSourceUtil.SOURCE_TRIGGER; public WiredTriggerHabboWalkOnFurni(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -36,24 +40,11 @@ public class WiredTriggerHabboWalkOnFurni extends InteractionWiredTrigger { @Override public boolean matches(HabboItem triggerItem, WiredEvent event) { HabboItem sourceItem = event.getSourceItem().orElse(null); - if (sourceItem == null) { - return false; - } - - if (this.items.contains(sourceItem)) { - return true; - } - Room room = event.getRoom(); - if (room != null) { - for (HabboItem item : room.getItemsAt(sourceItem.getX(), sourceItem.getY())) { - if (this.items.contains(item)) { - return true; - } - } - } - - return false; + return WiredTriggerSourceUtil.containsItemOrTile( + room, + WiredTriggerSourceUtil.resolveItems(this, event, this.furniSource, this.items), + sourceItem); } @Deprecated @@ -93,7 +84,8 @@ public class WiredTriggerHabboWalkOnFurni extends InteractionWiredTrigger { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(0); + message.appendInt(1); + message.appendInt(this.furniSource); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -102,12 +94,28 @@ public class WiredTriggerHabboWalkOnFurni extends InteractionWiredTrigger { @Override public boolean saveData(WiredSettings settings) { + return this.saveData(settings, null); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { this.items.clear(); + this.furniSource = (settings.getIntParams().length > 0) + ? this.normalizeFurniSource(settings.getIntParams()[0]) + : ((settings.getFurniIds().length > 0) ? WiredSourceUtil.SOURCE_SELECTED : WiredSourceUtil.SOURCE_TRIGGER); + + if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { + return true; + } int count = settings.getFurniIds().length; + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + if (room == null) { + return false; + } for (int i = 0; i < count; i++) { - HabboItem item = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()).getHabboItem(settings.getFurniIds()[i]); + HabboItem item = room.getHabboItem(settings.getFurniIds()[i]); if (item != null) { this.items.add(item); } @@ -119,6 +127,7 @@ public class WiredTriggerHabboWalkOnFurni extends InteractionWiredTrigger { @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( + this.furniSource, this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) )); } @@ -126,10 +135,12 @@ public class WiredTriggerHabboWalkOnFurni extends InteractionWiredTrigger { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { this.items.clear(); + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; String wiredData = set.getString("wired_data"); if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.furniSource = this.normalizeFurniSource(data.furniSource); for (Integer id: data.itemIds) { HabboItem item = room.getHabboItem(id); if (item != null) { @@ -155,12 +166,15 @@ public class WiredTriggerHabboWalkOnFurni extends InteractionWiredTrigger { } } } + + this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; } } @Override public void onPickUp() { this.items.clear(); + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; } @Override @@ -169,10 +183,20 @@ public class WiredTriggerHabboWalkOnFurni extends InteractionWiredTrigger { } static class JsonData { + int furniSource; List itemIds; - public JsonData(List itemIds) { + public JsonData(int furniSource, List itemIds) { + this.furniSource = furniSource; this.itemIds = itemIds; } } + + private int normalizeFurniSource(int value) { + if (value == WiredSourceUtil.SOURCE_SELECTED || value == WiredSourceUtil.SOURCE_SELECTOR) { + return value; + } + + return WiredSourceUtil.SOURCE_TRIGGER; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerReceiveSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerReceiveSignal.java index c2e96e93..0a860b2a 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerReceiveSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerReceiveSignal.java @@ -1,5 +1,7 @@ package com.eu.habbo.habbohotel.items.interactions.wired.triggers; +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; @@ -10,34 +12,108 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredTriggerType; import com.eu.habbo.habbohotel.wired.core.WiredEvent; import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import com.eu.habbo.habbohotel.wired.core.WiredTriggerSourceUtil; import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredTriggerSaveException; +import com.eu.habbo.messages.outgoing.rooms.items.ItemStateComposer; +import gnu.trove.set.hash.THashSet; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; public class WiredTriggerReceiveSignal extends InteractionWiredTrigger { public static final WiredTriggerType type = WiredTriggerType.RECEIVE_SIGNAL; + private static final long ACTIVATION_PULSE_MS = 300L; + private static final String ANTENNA_INTERACTION = "antenna"; + private static final String REQUIRE_ANTENNA_ERROR = "You can only select antenna furni."; + private int channel = 0; // signal channel (0-based) + private THashSet items; + private int furniSource = WiredSourceUtil.SOURCE_SELECTED; + private final AtomicLong activationToken = new AtomicLong(); public WiredTriggerReceiveSignal(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); + this.items = new THashSet<>(); } public WiredTriggerReceiveSignal(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { super(id, userId, item, extradata, limitedStack, limitedSells); + this.items = new THashSet<>(); } @Override public boolean matches(HabboItem triggerItem, WiredEvent event) { - return event.getType() == WiredEvent.Type.SIGNAL_RECEIVED - && event.getSignalChannel() == this.channel; + if (event.getType() != WiredEvent.Type.SIGNAL_RECEIVED) return false; + + List resolvedAntennas = WiredTriggerSourceUtil.resolveItems(this, event, this.furniSource, this.items).stream() + .filter(this::isAntennaItem) + .collect(Collectors.toList()); + + if (!resolvedAntennas.isEmpty()) { + return resolvedAntennas.stream() + .anyMatch(item -> item != null && item.getId() == event.getSignalChannel()); + } + + return this.channel > 0 && event.getSignalChannel() == this.channel; } public int getChannel() { return channel; } + public boolean unlinkAntenna(int antennaItemId) { + if (antennaItemId <= 0) { + return false; + } + + boolean changed = false; + + if (!this.items.isEmpty()) { + THashSet itemsToRemove = new THashSet<>(); + + for (HabboItem item : this.items) { + if (item == null || item.getId() == antennaItemId) { + itemsToRemove.add(item); + } + } + + if (!itemsToRemove.isEmpty()) { + this.items.removeAll(itemsToRemove); + changed = true; + } + } + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED) { + int nextChannel = 0; + + if (!this.items.isEmpty()) { + HabboItem firstItem = this.items.iterator().next(); + nextChannel = (firstItem != null) ? firstItem.getId() : 0; + } + + if (this.channel != nextChannel) { + this.channel = nextChannel; + changed = true; + } + } else if (this.channel == antennaItemId) { + this.channel = 0; + changed = true; + } + + if (changed) { + this.needsUpdate(true); + } + + return changed; + } + @Override public boolean isTriggeredByRoomUnit() { return false; @@ -59,21 +135,35 @@ public class WiredTriggerReceiveSignal extends InteractionWiredTrigger { int senderCount = 0; try { if (room != null && room.getRoomSpecialTypes() != null) { - senderCount = room.getRoomSpecialTypes().countSendersTargetingReceiver(this.getId()); + senderCount = this.getSenderCount(room); } } catch (Exception e) { } + THashSet itemsToRemove = new THashSet<>(); + for (HabboItem item : this.items) { + if (item.getRoomId() != this.getRoomId() || room.getHabboItem(item.getId()) == null) { + itemsToRemove.add(item); + } + } + for (HabboItem item : itemsToRemove) { + this.items.remove(item); + } + message.appendBoolean(false); - message.appendInt(0); - message.appendInt(0); + message.appendInt(WiredManager.MAXIMUM_FURNI_SELECTION); + message.appendInt(this.items.size()); + for (HabboItem item : this.items) { + message.appendInt(item.getId()); + } message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(3); + message.appendInt(4); message.appendInt(channel); message.appendInt(senderCount); message.appendInt(RoomSpecialTypes.MAX_SENDERS_PER_RECEIVER); + message.appendInt(this.furniSource); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -82,37 +172,164 @@ public class WiredTriggerReceiveSignal extends InteractionWiredTrigger { @Override public boolean saveData(WiredSettings settings) { + return this.saveData(settings, null); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + this.items.clear(); + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + int count = settings.getFurniIds().length; + int[] params = settings.getIntParams(); this.channel = params.length > 0 ? params[0] : 0; + this.furniSource = (params.length > 1) + ? this.normalizeFurniSource(params[params.length - 1]) + : WiredSourceUtil.SOURCE_SELECTED; + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED && room != null) { + for (int i = 0; i < count; i++) { + HabboItem item = room.getHabboItem(settings.getFurniIds()[i]); + if (item == null) continue; + if (!this.isAntennaItem(item)) throw new WiredTriggerSaveException(REQUIRE_ANTENNA_ERROR); + + this.items.add(item); + } + } + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED && !this.items.isEmpty()) { + HabboItem firstItem = this.items.iterator().next(); + this.channel = (firstItem != null) ? firstItem.getId() : this.channel; + } + return true; } + @Override + public void activateBox(Room room, RoomUnit roomUnit, long millis) { + if (roomUnit != null) { + this.addUserExecutionCache(roomUnit.getId(), millis); + } + + if (room == null || room.isHideWired() || this.getBaseItem().getStateCount() <= 1) { + return; + } + + final long token = this.activationToken.incrementAndGet(); + + if ("1".equals(this.getExtradata())) { + this.setExtradata("0"); + room.sendComposer(new ItemStateComposer(this).compose()); + } + + this.setExtradata("1"); + room.sendComposer(new ItemStateComposer(this).compose()); + + Emulator.getThreading().run(() -> { + if (!room.isLoaded()) return; + if (this.activationToken.get() != token) return; + + this.setExtradata("0"); + room.sendComposer(new ItemStateComposer(this).compose()); + }, ACTIVATION_PULSE_MS); + } + @Override public String getWiredData() { - return WiredManager.getGson().toJson(new JsonData(channel)); + return WiredManager.getGson().toJson(new JsonData( + channel, + furniSource, + this.items.stream().map(HabboItem::getId).collect(Collectors.toList()) + )); } @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.items = new THashSet<>(); String wiredData = set.getString("wired_data"); + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + if (wiredData != null && wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.channel = data.channel; + this.furniSource = this.normalizeFurniSource(data.furniSource); + if (data.itemIds != null) { + for (Integer id : data.itemIds) { + HabboItem item = room.getHabboItem(id); + if (item != null) this.items.add(item); + } + } + + if (this.furniSource != WiredSourceUtil.SOURCE_SELECTOR) this.furniSource = WiredSourceUtil.SOURCE_SELECTED; + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED && !this.items.isEmpty() && this.channel <= 0) { + HabboItem firstItem = this.items.iterator().next(); + if (firstItem != null) this.channel = firstItem.getId(); + } } } @Override public void onPickUp() { this.channel = 0; + this.items.clear(); + this.furniSource = WiredSourceUtil.SOURCE_SELECTED; } static class JsonData { int channel; + int furniSource; + List itemIds; public JsonData() {} - public JsonData(int channel) { + public JsonData(int channel, int furniSource, List itemIds) { this.channel = channel; + this.furniSource = furniSource; + this.itemIds = itemIds; } } + + private int normalizeFurniSource(int value) { + if (value == WiredSourceUtil.SOURCE_SELECTED || value == WiredSourceUtil.SOURCE_SELECTOR) { + return value; + } + + return WiredSourceUtil.SOURCE_SELECTED; + } + + private boolean isAntennaItem(HabboItem item) { + if (item == null || item.getBaseItem() == null || item.getBaseItem().getInteractionType() == null) { + return false; + } + + String interaction = item.getBaseItem().getInteractionType().getName(); + return interaction != null && ANTENNA_INTERACTION.equalsIgnoreCase(interaction); + } + + private int getSenderCount(Room room) { + if (room == null || room.getRoomSpecialTypes() == null) { + return 0; + } + + if (this.furniSource == WiredSourceUtil.SOURCE_SELECTED && !this.items.isEmpty()) { + List antennaIds = new ArrayList<>(); + + for (HabboItem item : this.items) { + if (this.isAntennaItem(item)) { + antennaIds.add(item.getId()); + } + } + + if (!antennaIds.isEmpty()) { + return room.getRoomSpecialTypes().countSendersTargetingAnyReceiver(antennaIds); + } + } + + if (this.channel > 0) { + return room.getRoomSpecialTypes().countSendersTargetingReceiver(this.channel); + } + + return 0; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java index e91a43f3..2ac70e88 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java @@ -144,7 +144,9 @@ public class WiredTriggerRepeater extends InteractionWiredTrigger implements Wir // Fire when elapsed time is a multiple of repeatTime if (elapsedMs % this.repeatTime == 0) { - if (this.getRoomId() != 0 && room.isLoaded()) { + long currentTime = System.currentTimeMillis(); + if (this.getRoomId() != 0 && room.isLoaded() + && WiredManager.isTriggerExecutionAllowed(room, this, currentTime)) { WiredManager.triggerTimerRepeat(room, this); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java index f9094c09..3986d5b8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java @@ -138,8 +138,10 @@ public class WiredTriggerRepeaterLong extends InteractionWiredTrigger implements // Fire when elapsed time is a multiple of repeat time if (elapsedMs % this.repeatTime == 0) { - if (this.getRoomId() != 0 && room.isLoaded()) { - WiredManager.triggerTimerRepeat(room, this); + long currentTime = System.currentTimeMillis(); + if (this.getRoomId() != 0 && room.isLoaded() + && WiredManager.isTriggerExecutionAllowed(room, this, currentTime)) { + WiredManager.triggerTimerRepeatLong(room, this); } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterShort.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterShort.java new file mode 100644 index 00000000..e20f5bae --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterShort.java @@ -0,0 +1,127 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.triggers; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.wired.WiredTriggerType; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import gnu.trove.procedure.TObjectProcedure; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class WiredTriggerRepeaterShort extends WiredTriggerRepeater { + public static final WiredTriggerType type = WiredTriggerType.PERIODICALLY_SHORT; + public static final int STEP_MS = 50; + public static final int DEFAULT_DELAY = 10 * STEP_MS; + public static final int MIN_DELAY = STEP_MS; + public static final int MAX_DELAY = 10 * STEP_MS; + + public WiredTriggerRepeaterShort(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + this.repeatTime = DEFAULT_DELAY; + } + + public WiredTriggerRepeaterShort(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + this.repeatTime = DEFAULT_DELAY; + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + String wiredData = set.getString("wired_data"); + + if (wiredData != null && wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + this.repeatTime = (data != null) ? data.repeatTime : DEFAULT_DELAY; + } else if (wiredData != null && wiredData.length() >= 1) { + this.repeatTime = Integer.parseInt(wiredData); + } else { + this.repeatTime = DEFAULT_DELAY; + } + + this.repeatTime = clampRepeatTime(this.repeatTime); + } + + @Override + public void onPickUp() { + this.repeatTime = DEFAULT_DELAY; + } + + @Override + public WiredTriggerType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(5); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(""); + message.appendInt(1); + message.appendInt(this.repeatTime / STEP_MS); + message.appendInt(0); + message.appendInt(this.getType().code); + + if (!this.isTriggeredByRoomUnit()) { + List invalidTriggers = new ArrayList<>(); + room.getRoomSpecialTypes().getEffects(this.getX(), this.getY()).forEach(new TObjectProcedure() { + @Override + public boolean execute(InteractionWiredEffect object) { + if (object.requiresTriggeringUser()) { + invalidTriggers.add(object.getBaseItem().getSpriteId()); + } + return true; + } + }); + message.appendInt(invalidTriggers.size()); + for (Integer i : invalidTriggers) { + message.appendInt(i); + } + } else { + message.appendInt(0); + } + } + + @Override + public boolean saveData(WiredSettings settings) { + if (settings.getIntParams().length < 1) return false; + + int newRepeatTime = settings.getIntParams()[0] * STEP_MS; + this.repeatTime = clampRepeatTime(newRepeatTime); + + return true; + } + + @Override + public void onWiredTick(Room room, long tickCount, int tickIntervalMs) { + long elapsedMs = tickCount * tickIntervalMs; + + if (elapsedMs % this.repeatTime == 0) { + long currentTime = System.currentTimeMillis(); + if (this.getRoomId() != 0 && room.isLoaded() + && WiredManager.isTriggerExecutionAllowed(room, this, currentTime)) { + WiredManager.triggerTimerRepeatShort(room, this); + } + } + } + + private int clampRepeatTime(int repeatTime) { + if (repeatTime < MIN_DELAY) { + return DEFAULT_DELAY; + } + + if (repeatTime > MAX_DELAY) { + return MAX_DELAY; + } + + return repeatTime; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerScoreAchieved.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerScoreAchieved.java index 179e9597..8bf9c7b2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerScoreAchieved.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerScoreAchieved.java @@ -1,10 +1,12 @@ package com.eu.habbo.habbohotel.items.interactions.wired.triggers; +import com.eu.habbo.habbohotel.games.GameTeamColors; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; 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.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.WiredTriggerType; @@ -17,6 +19,7 @@ import java.sql.SQLException; public class WiredTriggerScoreAchieved extends InteractionWiredTrigger { private static final WiredTriggerType type = WiredTriggerType.SCORE_ACHIEVED; private int score = 0; + private int teamType = GameTeamColors.NONE.type; public WiredTriggerScoreAchieved(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -32,7 +35,23 @@ public class WiredTriggerScoreAchieved extends InteractionWiredTrigger { int amountAdded = event.getScoreAdded(); // Check if this score addition crossed the threshold - return points - amountAdded < this.score && points >= this.score; + if (!(points - amountAdded < this.score && points >= this.score)) { + return false; + } + + if (this.teamType == GameTeamColors.NONE.type) { + return true; + } + + if (!event.getActor().isPresent()) { + return false; + } + + Habbo habbo = event.getRoom().getHabbo(event.getActor().get()); + + return habbo != null + && habbo.getHabboInfo().getGamePlayer() != null + && habbo.getHabboInfo().getGamePlayer().getTeamColor().type == this.teamType; } @Deprecated @@ -44,7 +63,8 @@ public class WiredTriggerScoreAchieved extends InteractionWiredTrigger { @Override public String getWiredData() { return WiredManager.getGson().toJson(new JsonData( - this.score + this.score, + this.teamType )); } @@ -55,17 +75,20 @@ public class WiredTriggerScoreAchieved extends InteractionWiredTrigger { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.score = data.score; + this.teamType = normalizeTeamType(data.teamType); } else { try { this.score = Integer.parseInt(wiredData); } catch (Exception e) { } + this.teamType = GameTeamColors.NONE.type; } } @Override public void onPickUp() { this.score = 0; + this.teamType = GameTeamColors.NONE.type; } @Override @@ -81,8 +104,9 @@ public class WiredTriggerScoreAchieved extends InteractionWiredTrigger { message.appendInt(this.getBaseItem().getSpriteId()); message.appendInt(this.getId()); message.appendString(""); - message.appendInt(1); + message.appendInt(2); message.appendInt(this.score); + message.appendInt(this.teamType); message.appendInt(0); message.appendInt(this.getType().code); message.appendInt(0); @@ -93,6 +117,9 @@ public class WiredTriggerScoreAchieved extends InteractionWiredTrigger { public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 1) return false; this.score = settings.getIntParams()[0]; + this.teamType = (settings.getIntParams().length > 1) + ? normalizeTeamType(settings.getIntParams()[1]) + : GameTeamColors.NONE.type; return true; } @@ -101,11 +128,21 @@ public class WiredTriggerScoreAchieved extends InteractionWiredTrigger { return true; } + private int normalizeTeamType(int value) { + if (value >= GameTeamColors.RED.type && value <= GameTeamColors.YELLOW.type) { + return value; + } + + return GameTeamColors.NONE.type; + } + static class JsonData { int score; + int teamType; - public JsonData(int score) { + public JsonData(int score, int teamType) { this.score = score; + this.teamType = teamType; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerVariableChanged.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerVariableChanged.java new file mode 100644 index 00000000..c638c12f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerVariableChanged.java @@ -0,0 +1,302 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.triggers; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredTriggerType; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.incoming.wired.WiredTriggerSaveException; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WiredTriggerVariableChanged extends InteractionWiredTrigger { + public static final WiredTriggerType type = WiredTriggerType.VARIABLE_CHANGED; + + public static final int TARGET_USER = 0; + public static final int TARGET_FURNI = 1; + public static final int TARGET_ROOM = 3; + + private static final String CUSTOM_TOKEN_PREFIX = "custom:"; + + private String variableToken = ""; + private int variableItemId = 0; + private int targetType = TARGET_USER; + private boolean createdEnabled = true; + private boolean valueChangedEnabled = true; + private boolean increasedEnabled = true; + private boolean decreasedEnabled = true; + private boolean unchangedEnabled = true; + private boolean deletedEnabled = true; + + public WiredTriggerVariableChanged(ResultSet set, Item baseItem) throws SQLException { + super(set, baseItem); + } + + public WiredTriggerVariableChanged(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { + super(id, userId, item, extradata, limitedStack, limitedSells); + } + + @Override + public boolean matches(HabboItem triggerItem, WiredEvent event) { + if (event == null || event.getType() != WiredEvent.Type.VARIABLE_CHANGED) { + return false; + } + + if (event.getVariableTargetType() != this.targetType || event.getVariableDefinitionItemId() != this.variableItemId) { + return false; + } + + if (this.createdEnabled && event.isVariableCreated()) { + return true; + } + + if (this.deletedEnabled && event.isVariableDeleted()) { + return true; + } + + if (!this.valueChangedEnabled) { + return false; + } + + return switch (event.getVariableChangeKind()) { + case INCREASED -> this.increasedEnabled; + case DECREASED -> this.decreasedEnabled; + case UNCHANGED -> this.unchangedEnabled; + default -> false; + }; + } + + @Deprecated + @Override + public boolean execute(RoomUnit roomUnit, Room room, Object[] stuff) { + return false; + } + + @Override + public WiredTriggerType getType() { + return type; + } + + @Override + public void serializeWiredData(ServerMessage message, Room room) { + message.appendBoolean(false); + message.appendInt(0); + message.appendInt(0); + message.appendInt(this.getBaseItem().getSpriteId()); + message.appendInt(this.getId()); + message.appendString(this.variableToken == null ? "" : this.variableToken); + message.appendInt(7); + message.appendInt(this.targetType); + message.appendInt(this.createdEnabled ? 1 : 0); + message.appendInt(this.valueChangedEnabled ? 1 : 0); + message.appendInt(this.increasedEnabled ? 1 : 0); + message.appendInt(this.decreasedEnabled ? 1 : 0); + message.appendInt(this.unchangedEnabled ? 1 : 0); + message.appendInt(this.deletedEnabled ? 1 : 0); + message.appendInt(0); + message.appendInt(this.getType().code); + message.appendInt(0); + message.appendInt(0); + } + + @Override + public boolean saveData(WiredSettings settings) { + return this.saveData(settings, null); + } + + @Override + public boolean saveData(WiredSettings settings, GameClient gameClient) { + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); + int[] params = settings.getIntParams(); + + this.targetType = normalizeTargetType((params.length > 0) ? params[0] : TARGET_USER); + this.createdEnabled = (params.length <= 1) || (params[1] == 1); + this.valueChangedEnabled = (params.length <= 2) || (params[2] == 1); + this.increasedEnabled = (params.length <= 3) || (params[3] == 1); + this.decreasedEnabled = (params.length <= 4) || (params[4] == 1); + this.unchangedEnabled = (params.length <= 5) || (params[5] == 1); + this.deletedEnabled = (params.length <= 6) || (params[6] == 1); + this.setVariableToken(normalizeVariableToken(settings.getStringParam())); + this.normalizeOptions(); + + if (this.variableItemId <= 0) { + throw new WiredTriggerSaveException("wiredfurni.params.variables.validation.missing_variable"); + } + + if (!this.hasAnyEnabledOption()) { + return false; + } + + if (room == null || !this.isValidDefinition(room)) { + throw new WiredTriggerSaveException("wiredfurni.params.variables.validation.invalid_variable"); + } + + return true; + } + + @Override + public String getWiredData() { + return WiredManager.getGson().toJson(new JsonData( + this.variableToken, + this.variableItemId, + this.targetType, + this.createdEnabled, + this.valueChangedEnabled, + this.increasedEnabled, + this.decreasedEnabled, + this.unchangedEnabled, + this.deletedEnabled + )); + } + + @Override + public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + + if (!wiredData.startsWith("{")) { + this.setVariableToken(normalizeVariableToken(wiredData)); + return; + } + + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + + this.targetType = normalizeTargetType(data.targetType); + this.createdEnabled = data.createdEnabled; + this.valueChangedEnabled = data.valueChangedEnabled; + this.increasedEnabled = data.increasedEnabled; + this.decreasedEnabled = data.decreasedEnabled; + this.unchangedEnabled = data.unchangedEnabled; + this.deletedEnabled = data.deletedEnabled; + this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); + this.normalizeOptions(); + } + + @Override + public void onPickUp() { + this.variableToken = ""; + this.variableItemId = 0; + this.targetType = TARGET_USER; + this.createdEnabled = true; + this.valueChangedEnabled = true; + this.increasedEnabled = true; + this.decreasedEnabled = true; + this.unchangedEnabled = true; + this.deletedEnabled = true; + } + + private void setVariableToken(String token) { + this.variableToken = normalizeVariableToken(token); + this.variableItemId = getCustomItemId(this.variableToken); + } + + private void normalizeOptions() { + if (!this.valueChangedEnabled) { + this.increasedEnabled = false; + this.decreasedEnabled = false; + this.unchangedEnabled = false; + } + + if (this.targetType == TARGET_ROOM) { + this.createdEnabled = false; + this.deletedEnabled = false; + } + } + + private boolean hasAnyEnabledOption() { + return this.createdEnabled + || this.deletedEnabled + || (this.valueChangedEnabled && (this.increasedEnabled || this.decreasedEnabled || this.unchangedEnabled)); + } + + private boolean isValidDefinition(Room room) { + WiredVariableDefinitionInfo definitionInfo = switch (this.targetType) { + case TARGET_FURNI -> room.getFurniVariableManager().getDefinitionInfo(this.variableItemId); + case TARGET_ROOM -> room.getRoomVariableManager().getDefinitionInfo(this.variableItemId); + default -> room.getUserVariableManager().getDefinitionInfo(this.variableItemId); + }; + + return definitionInfo != null; + } + + private static int normalizeTargetType(int value) { + return switch (value) { + case TARGET_FURNI, TARGET_ROOM -> value; + default -> TARGET_USER; + }; + } + + private static String normalizeVariableToken(String token) { + if (token == null) { + return ""; + } + + String normalized = token.trim(); + if (normalized.isEmpty()) { + return ""; + } + + if (normalized.startsWith(CUSTOM_TOKEN_PREFIX)) { + return normalized; + } + + try { + int itemId = Integer.parseInt(normalized); + return (itemId > 0) ? (CUSTOM_TOKEN_PREFIX + itemId) : ""; + } catch (NumberFormatException ignored) { + return ""; + } + } + + private static int getCustomItemId(String token) { + if (token == null || !token.startsWith(CUSTOM_TOKEN_PREFIX)) { + return 0; + } + + try { + return Integer.parseInt(token.substring(CUSTOM_TOKEN_PREFIX.length())); + } catch (NumberFormatException ignored) { + return 0; + } + } + + static class JsonData { + String variableToken; + int variableItemId; + int targetType; + boolean createdEnabled; + boolean valueChangedEnabled; + boolean increasedEnabled; + boolean decreasedEnabled; + boolean unchangedEnabled; + boolean deletedEnabled; + + JsonData(String variableToken, int variableItemId, int targetType, boolean createdEnabled, boolean valueChangedEnabled, boolean increasedEnabled, boolean decreasedEnabled, boolean unchangedEnabled, boolean deletedEnabled) { + this.variableToken = variableToken; + this.variableItemId = variableItemId; + this.targetType = targetType; + this.createdEnabled = createdEnabled; + this.valueChangedEnabled = valueChangedEnabled; + this.increasedEnabled = increasedEnabled; + this.decreasedEnabled = decreasedEnabled; + this.unchangedEnabled = unchangedEnabled; + this.deletedEnabled = deletedEnabled; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Message.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Message.java index a081bfc5..727729b6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Message.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Message.java @@ -1,15 +1,14 @@ package com.eu.habbo.habbohotel.messenger; import com.eu.habbo.Emulator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.eu.habbo.core.DatabaseLoggable; -import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; -public class Message implements Runnable { - private static final Logger LOGGER = LoggerFactory.getLogger(Message.class); +public class Message implements Runnable, DatabaseLoggable { + + private static final String QUERY = "INSERT INTO chatlogs_private (user_from_id, user_to_id, message, timestamp) VALUES (?, ?, ?, ?)"; private final int fromId; private final int toId; @@ -26,20 +25,25 @@ public class Message implements Runnable { @Override public void run() { - //TODO Turn into scheduler if (Messenger.SAVE_PRIVATE_CHATS) { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO chatlogs_private (user_from_id, user_to_id, message, timestamp) VALUES (?, ?, ?, ?)")) { - statement.setInt(1, this.fromId); - statement.setInt(2, this.toId); - statement.setString(3, this.message); - statement.setInt(4, this.timestamp); - statement.execute(); - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } + Emulator.getDatabaseLogger().store(this); } } + @Override + public String getQuery() { + return QUERY; + } + + @Override + public void log(PreparedStatement statement) throws SQLException { + statement.setInt(1, this.fromId); + statement.setInt(2, this.toId); + statement.setString(3, this.message); + statement.setInt(4, this.timestamp); + statement.addBatch(); + } + public int getToId() { return this.toId; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java index 0227d0b3..1984ad28 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java @@ -54,11 +54,23 @@ public class ModToolManager { if (userId <= 0) return; - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.*, users_settings.*, permissions.rank_name, permissions.acc_hide_mail AS hide_mail, permissions.id AS rank_id FROM users INNER JOIN users_settings ON users.id = users_settings.user_id INNER JOIN permissions ON permissions.id = users.rank WHERE users.id = ? LIMIT 1")) { + String query = Emulator.getGameEnvironment().getPermissionsManager().isNormalizedSchemaEnabled() + ? "SELECT users.*, users_settings.*, permission_ranks.rank_name, permission_ranks.id AS rank_id " + + "FROM users " + + "INNER JOIN users_settings ON users.id = users_settings.user_id " + + "INNER JOIN permission_ranks ON permission_ranks.id = users.rank " + + "WHERE users.id = ? LIMIT 1" + : "SELECT users.*, users_settings.*, permissions.rank_name, permissions.acc_hide_mail AS hide_mail, permissions.id AS rank_id FROM users INNER JOIN users_settings ON users.id = users_settings.user_id INNER JOIN permissions ON permissions.id = users.rank WHERE users.id = ? LIMIT 1"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement(query)) { statement.setInt(1, userId); try (ResultSet set = statement.executeQuery()) { while (set.next()) { - client.sendResponse(new ModToolUserInfoComposer(set)); + boolean hideMail = Emulator.getGameEnvironment().getPermissionsManager().isNormalizedSchemaEnabled() + ? Emulator.getGameEnvironment().getPermissionsManager().getRank(set.getInt("rank_id")).hasPermission("acc_hide_mail", false) + : set.getBoolean("hide_mail"); + + client.sendResponse(new ModToolUserInfoComposer(set, hideMail)); } } } catch (SQLException e) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Permission.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Permission.java index 242db3fb..11e042e2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Permission.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Permission.java @@ -31,6 +31,7 @@ public class Permission { public static String ACC_NOMUTE = "acc_nomute"; public static String ACC_GUILD_ADMIN = "acc_guild_admin"; public static String ACC_CATALOG_IDS = "acc_catalog_ids"; + public static String ACC_CATALOGFURNI = "acc_catalogfurni"; public static String ACC_MODTOOL_TICKET_Q = "acc_modtool_ticket_q"; public static String ACC_MODTOOL_USER_LOGS = "acc_modtool_user_logs"; public static String ACC_MODTOOL_USER_ALERT = "acc_modtool_user_alert"; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionsManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionsManager.java index b9d24931..0286a468 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionsManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionsManager.java @@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory; import java.sql.*; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -20,6 +21,7 @@ public class PermissionsManager { private final TIntObjectHashMap ranks; private final TIntIntHashMap enables; private final THashMap> badges; + private volatile boolean normalizedSchemaEnabled; public PermissionsManager() { long millis = System.currentTimeMillis(); @@ -40,7 +42,30 @@ public class PermissionsManager { private void loadPermissions() { this.badges.clear(); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM permissions ORDER BY id ASC")) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + if (this.hasNormalizedPermissionsSchema(connection)) { + try { + if (this.loadPermissionsNormalized(connection)) { + this.normalizedSchemaEnabled = true; + LOGGER.info("Permissions Manager -> Using normalized permissions schema."); + return; + } + } catch (SQLException e) { + LOGGER.warn("Permissions Manager -> Failed to load normalized permissions schema, falling back to legacy permissions table.", e); + } + } + + this.normalizedSchemaEnabled = false; + this.badges.clear(); + LOGGER.info("Permissions Manager -> Using legacy permissions schema."); + this.loadPermissionsLegacy(connection); + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + } + + private void loadPermissionsLegacy(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM permissions ORDER BY id ASC")) { while (set.next()) { Rank rank = null; if (!this.ranks.containsKey(set.getInt("id"))) { @@ -51,16 +76,135 @@ public class PermissionsManager { rank.load(set); } - if (rank != null && !rank.getBadge().isEmpty()) { - if (!this.badges.containsKey(rank.getBadge())) { - this.badges.put(rank.getBadge(), new ArrayList()); - } + this.addBadgeMapping(rank); + } + } + } - this.badges.get(rank.getBadge()).add(rank); + private boolean loadPermissionsNormalized(Connection connection) throws SQLException { + boolean hasRanks = false; + List loadedRanks = new ArrayList<>(); + + try (Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM permission_ranks ORDER BY id ASC")) { + while (set.next()) { + hasRanks = true; + + Rank rank = this.ranks.get(set.getInt("id")); + + if (rank == null) { + rank = new Rank(set.getInt("id")); + this.ranks.put(set.getInt("id"), rank); + } + + rank.loadNormalizedMetadata(set); + this.addBadgeMapping(rank); + loadedRanks.add(rank); + } + } + + if (!hasRanks) { + return false; + } + + this.ensureNormalizedRankColumns(connection, loadedRanks); + + boolean hasDefinitions = false; + + try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM permission_definitions ORDER BY permission_key ASC"); + ResultSet set = statement.executeQuery()) { + ResultSetMetaData meta = set.getMetaData(); + Set availableColumns = new HashSet<>(); + + for (int i = 1; i <= meta.getColumnCount(); i++) { + availableColumns.add(meta.getColumnName(i).toLowerCase()); + } + + for (Rank rank : loadedRanks) { + if (!availableColumns.contains(("rank_" + rank.getId()).toLowerCase())) { + return false; } } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); + + while (set.next()) { + hasDefinitions = true; + String permissionKey = set.getString("permission_key"); + + for (Rank rank : loadedRanks) { + String rankColumn = "rank_" + rank.getId(); + + if (!availableColumns.contains(rankColumn.toLowerCase())) { + continue; + } + + rank.setPermission(permissionKey, PermissionSetting.fromString(Integer.toString(set.getInt(rankColumn)))); + } + } + } + + return hasDefinitions; + } + + private void ensureNormalizedRankColumns(Connection connection, List loadedRanks) throws SQLException { + Set availableColumns = new HashSet<>(); + + try (PreparedStatement statement = connection.prepareStatement("SELECT column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'permission_definitions'"); + ResultSet set = statement.executeQuery()) { + while (set.next()) { + availableColumns.add(set.getString("column_name").toLowerCase()); + } + } + + for (Rank rank : loadedRanks) { + String rankColumn = "rank_" + rank.getId(); + + if (availableColumns.contains(rankColumn.toLowerCase())) { + continue; + } + + try (Statement statement = connection.createStatement()) { + statement.execute("ALTER TABLE permission_definitions ADD COLUMN `" + rankColumn + "` tinyint(3) unsigned NOT NULL DEFAULT 0"); + } + + availableColumns.add(rankColumn.toLowerCase()); + LOGGER.info("Permissions Manager -> Added missing normalized permission column {}.", rankColumn); + } + } + + private boolean hasNormalizedPermissionsSchema(Connection connection) throws SQLException { + if (!this.tableExists(connection, "permission_ranks") || !this.tableExists(connection, "permission_definitions")) { + return false; + } + + if (!this.tableHasRows(connection, "permission_ranks")) { + return false; + } + + return this.tableHasRows(connection, "permission_definitions"); + } + + private boolean tableExists(Connection connection, String tableName) throws SQLException { + try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?")) { + statement.setString(1, tableName); + + try (ResultSet set = statement.executeQuery()) { + return set.next() && set.getInt(1) > 0; + } + } + } + + private boolean tableHasRows(Connection connection, String tableName) throws SQLException { + try (Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT COUNT(*) FROM " + tableName)) { + return set.next() && set.getInt(1) > 0; + } + } + + private void addBadgeMapping(Rank rank) { + if (rank != null && !rank.getBadge().isEmpty()) { + if (!this.badges.containsKey(rank.getBadge())) { + this.badges.put(rank.getBadge(), new ArrayList()); + } + + this.badges.get(rank.getBadge()).add(rank); } } @@ -139,4 +283,8 @@ public class PermissionsManager { public List getAllRanks() { return new ArrayList<>(this.ranks.valueCollection()); } + + public boolean isNormalizedSchemaEnabled() { + return this.normalizedSchemaEnabled; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Rank.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Rank.java index cbbc01dc..897e5b5b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Rank.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Rank.java @@ -35,32 +35,29 @@ public class Rank { private int gotwTimerAmount; public Rank(ResultSet set) throws SQLException { + this(set.getInt("id")); + this.load(set); + } + + public Rank(int id) { this.permissions = new THashMap<>(); this.variables = new THashMap<>(); - this.id = set.getInt("id"); - this.level = set.getInt("level"); + this.id = id; + this.level = 1; this.diamondsTimerAmount = 1; this.creditsTimerAmount = 1; this.pixelsTimerAmount = 1; this.gotwTimerAmount = 1; - - this.load(set); } public void load(ResultSet set) throws SQLException { + this.permissions.clear(); + this.variables.clear(); + + this.loadMetadata(set); + ResultSetMetaData meta = set.getMetaData(); - this.name = set.getString("rank_name"); - this.badge = set.getString("badge"); - this.roomEffect = set.getInt("room_effect"); - this.logCommands = set.getString("log_commands").equals("1"); - this.prefix = set.getString("prefix"); - this.prefixColor = set.getString("prefix_color"); - this.level = set.getInt("level"); - this.diamondsTimerAmount = set.getInt("auto_points_amount"); - this.creditsTimerAmount = set.getInt("auto_credits_amount"); - this.pixelsTimerAmount = set.getInt("auto_pixels_amount"); - this.gotwTimerAmount = set.getInt("auto_gotw_amount"); - this.hasPrefix = !this.prefix.isEmpty(); + for (int i = 1; i < meta.getColumnCount() + 1; i++) { String columnName = meta.getColumnName(i); if (columnName.startsWith("cmd_") || columnName.startsWith("acc_")) { @@ -71,6 +68,51 @@ public class Rank { } } + public void loadNormalizedMetadata(ResultSet set) throws SQLException { + this.permissions.clear(); + this.variables.clear(); + this.loadMetadata(set); + this.storeMetadataVariables(); + } + + public void setPermission(String key, PermissionSetting setting) { + this.permissions.put(key, new Permission(key, setting)); + } + + private void loadMetadata(ResultSet set) throws SQLException { + this.name = this.safeString(set.getString("rank_name")); + this.badge = this.safeString(set.getString("badge")); + this.roomEffect = set.getInt("room_effect"); + this.logCommands = "1".equals(this.safeString(set.getString("log_commands"))); + this.prefix = this.safeString(set.getString("prefix")); + this.prefixColor = this.safeString(set.getString("prefix_color")); + this.level = set.getInt("level"); + this.diamondsTimerAmount = set.getInt("auto_points_amount"); + this.creditsTimerAmount = set.getInt("auto_credits_amount"); + this.pixelsTimerAmount = set.getInt("auto_pixels_amount"); + this.gotwTimerAmount = set.getInt("auto_gotw_amount"); + this.hasPrefix = !this.prefix.isEmpty(); + } + + private void storeMetadataVariables() { + this.variables.put("id", Integer.toString(this.id)); + this.variables.put("rank_name", this.name); + this.variables.put("badge", this.badge); + this.variables.put("room_effect", Integer.toString(this.roomEffect)); + this.variables.put("log_commands", this.logCommands ? "1" : "0"); + this.variables.put("prefix", this.prefix); + this.variables.put("prefix_color", this.prefixColor); + this.variables.put("level", Integer.toString(this.level)); + this.variables.put("auto_points_amount", Integer.toString(this.diamondsTimerAmount)); + this.variables.put("auto_credits_amount", Integer.toString(this.creditsTimerAmount)); + this.variables.put("auto_pixels_amount", Integer.toString(this.pixelsTimerAmount)); + this.variables.put("auto_gotw_amount", Integer.toString(this.gotwTimerAmount)); + } + + private String safeString(String value) { + return value == null ? "" : value; + } + public boolean hasPermission(String key, boolean isRoomOwner) { if (this.permissions.containsKey(key)) { Permission permission = this.permissions.get(key); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/BuildersClubRoomSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/BuildersClubRoomSupport.java new file mode 100644 index 00000000..c0227618 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/BuildersClubRoomSupport.java @@ -0,0 +1,574 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.guilds.Guild; +import com.eu.habbo.habbohotel.guilds.GuildMember; +import com.eu.habbo.habbohotel.guilds.GuildRank; +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.subscriptions.Subscription; +import com.eu.habbo.messages.outgoing.catalog.BuildersClubFurniCountComposer; +import com.eu.habbo.messages.outgoing.catalog.BuildersClubSubscriptionStatusComposer; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; +import com.eu.habbo.messages.outgoing.generic.alerts.SimpleAlertComposer; +import gnu.trove.map.hash.THashMap; +import gnu.trove.set.hash.THashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class BuildersClubRoomSupport { + private static final Logger LOGGER = LoggerFactory.getLogger(BuildersClubRoomSupport.class); + + public static final int DEFAULT_TRIAL_FURNI_LIMIT = 50; + // Runtime-only owner marker used to display Builders Club furni as virtual/non-user-owned in-room. + // The actual DB owner for persistence/FK purposes is tracked separately on the item instance. + public static final int VIRTUAL_OWNER_ID = 1; + public static final String DISPLAY_OWNER_NAME = "Builders Club"; + + public enum SyncResult { + UNCHANGED, + LOCKED, + UNLOCKED + } + + private BuildersClubRoomSupport() { + } + + public static int getFurniLimit(Habbo habbo) { + if (habbo == null) { + return DEFAULT_TRIAL_FURNI_LIMIT; + } + + return DEFAULT_TRIAL_FURNI_LIMIT + Math.max(0, habbo.getHabboStats().getBuildersClubBonusFurni()); + } + + public static int getFurniLimit(int userId) { + HabboInfo habboInfo = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId); + + if (habboInfo == null || habboInfo.getHabboStats() == null) { + return DEFAULT_TRIAL_FURNI_LIMIT; + } + + return DEFAULT_TRIAL_FURNI_LIMIT + Math.max(0, habboInfo.getHabboStats().getBuildersClubBonusFurni()); + } + + public static int getMembershipSecondsLeft(int userId) { + HabboInfo habboInfo = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId); + + if (habboInfo == null || habboInfo.getHabboStats() == null) { + return 0; + } + + Subscription subscription = habboInfo.getHabboStats().getSubscription(Subscription.BUILDERS_CLUB); + + if (subscription == null) { + return 0; + } + + return Math.max(0, subscription.getRemaining()); + } + + public static boolean hasActiveMembership(int userId) { + HabboInfo habboInfo = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId); + + return habboInfo != null + && habboInfo.getHabboStats() != null + && habboInfo.getHabboStats().hasSubscription(Subscription.BUILDERS_CLUB); + } + + public static int getTrackedFurniCount(int userId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM builders_club_items WHERE user_id = ? AND room_id > 0")) { + statement.setInt(1, userId); + + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + return set.getInt(1); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception counting Builders Club furni", e); + } + + return 0; + } + + public static boolean hasTrackedItemsInOwnedRooms(int ownerId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT 1 FROM builders_club_items bci INNER JOIN rooms r ON r.id = bci.room_id WHERE r.owner_id = ? AND bci.room_id > 0 LIMIT 1")) { + statement.setInt(1, ownerId); + + try (ResultSet set = statement.executeQuery()) { + return set.next(); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception checking Builders Club room ownership", e); + } + + return false; + } + + public static boolean roomHasTrackedItems(int roomId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT 1 FROM builders_club_items WHERE room_id = ? LIMIT 1")) { + statement.setInt(1, roomId); + + try (ResultSet set = statement.executeQuery()) { + return set.next(); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception checking Builders Club room items", e); + } + + return false; + } + + public static boolean isTrackedItem(int itemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT 1 FROM builders_club_items WHERE item_id = ? LIMIT 1")) { + statement.setInt(1, itemId); + + try (ResultSet set = statement.executeQuery()) { + return set.next(); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception checking Builders Club tracked item", e); + } + + return false; + } + + public static int getTrackedUserId(int itemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT user_id FROM builders_club_items WHERE item_id = ? LIMIT 1")) { + statement.setInt(1, itemId); + + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + return set.getInt("user_id"); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception getting Builders Club tracked user", e); + } + + return 0; + } + + public static boolean hasPlacementVisitors(Room room, Habbo owner) { + if (room == null || owner == null) { + return false; + } + + for (Habbo habbo : room.getHabbos()) { + if (habbo == null || habbo.getHabboInfo() == null) { + continue; + } + + if (habbo.getHabboInfo().getId() == owner.getHabboInfo().getId()) { + continue; + } + + if (habbo.hasPermission(Permission.ACC_ENTERANYROOM) || habbo.hasPermission(Permission.ACC_ANYROOMOWNER)) { + continue; + } + + return true; + } + + return false; + } + + public static boolean isPlacementBlockedByVisitors(Habbo habbo) { + if (habbo == null || habbo.getHabboInfo() == null) { + return false; + } + + if (hasActiveMembership(habbo.getHabboInfo().getId())) { + return false; + } + + Room currentRoom = habbo.getHabboInfo().getCurrentRoom(); + + if (currentRoom == null || currentRoom.getOwnerId() != habbo.getHabboInfo().getId()) { + return false; + } + + return hasPlacementVisitors(currentRoom, habbo); + } + + public static boolean canPlaceInCurrentRoom(Habbo habbo) { + if (habbo == null || habbo.getHabboInfo() == null || habbo.getHabboInfo().getCurrentRoom() == null) { + return false; + } + + return canPlaceInRoom(habbo, habbo.getHabboInfo().getCurrentRoom()); + } + + public static boolean canPlaceInRoom(Habbo habbo, Room room) { + if (habbo == null || habbo.getHabboInfo() == null || room == null) { + return false; + } + + if (room.getOwnerId() == habbo.getHabboInfo().getId()) { + return true; + } + + Room currentRoom = habbo.getHabboInfo().getCurrentRoom(); + + if (currentRoom == null || currentRoom.getId() != room.getId()) { + return false; + } + + return canUseGuildPlacementPool(habbo, room); + } + + public static int getPlacementPoolUserId(Habbo habbo) { + if (habbo == null || habbo.getHabboInfo() == null) { + return 0; + } + + Room currentRoom = habbo.getHabboInfo().getCurrentRoom(); + + if (currentRoom == null) { + return habbo.getHabboInfo().getId(); + } + + if (currentRoom.getOwnerId() == habbo.getHabboInfo().getId()) { + return habbo.getHabboInfo().getId(); + } + + if (canUseGuildPlacementPool(habbo, currentRoom)) { + Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(currentRoom.getGuildId()); + + if (guild != null && guild.getOwnerId() > 0) { + return guild.getOwnerId(); + } + } + + return habbo.getHabboInfo().getId(); + } + + public static int getPlacementPoolFurniCount(Habbo habbo) { + int userId = getPlacementPoolUserId(habbo); + + if (userId <= 0) { + return 0; + } + + return getTrackedFurniCount(userId); + } + + public static int getPlacementPoolFurniLimit(Habbo habbo) { + int userId = getPlacementPoolUserId(habbo); + + if (userId <= 0) { + return DEFAULT_TRIAL_FURNI_LIMIT; + } + + return getFurniLimit(userId); + } + + public static void sendPlacementStatus(Habbo habbo) { + if (habbo == null || habbo.getClient() == null) { + return; + } + + habbo.getClient().sendResponse(new BuildersClubFurniCountComposer(getTrackedFurniCount(habbo.getHabboInfo().getId()))); + habbo.getClient().sendResponse(new BuildersClubSubscriptionStatusComposer(habbo)); + } + + public static void sendPlacementStatusForPool(Room room, int placementUserId) { + if (placementUserId <= 0) { + return; + } + + THashSet updatedUsers = new THashSet<>(); + + if (room != null) { + for (Habbo habbo : room.getHabbos()) { + if (habbo == null || habbo.getHabboInfo() == null) { + continue; + } + + if (getPlacementPoolUserId(habbo) != placementUserId) { + continue; + } + + sendPlacementStatus(habbo); + updatedUsers.add(habbo.getHabboInfo().getId()); + } + } + + Habbo placementPoolHabbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(placementUserId); + + if (placementPoolHabbo != null && placementPoolHabbo.getHabboInfo() != null && !updatedUsers.contains(placementPoolHabbo.getHabboInfo().getId())) { + sendPlacementStatus(placementPoolHabbo); + } + } + + public static void sendCurrentRoomPlacementStatus(Room room) { + if (room == null) { + return; + } + + Habbo owner = room.getHabbo(room.getOwnerId()); + + if (owner == null || owner.getClient() == null) { + return; + } + + owner.getClient().sendResponse(new com.eu.habbo.messages.outgoing.catalog.BuildersClubSubscriptionStatusComposer(owner)); + } + + private static boolean canUseGuildPlacementPool(Habbo habbo, Room room) { + if (habbo == null || room == null) { + return false; + } + + Guild guild = resolvePlacementGuild(room); + + if (guild == null || guild.getOwnerId() <= 0) { + return false; + } + + boolean isGuildAdmin = room.getGuildRightLevel(habbo).isEqualOrGreaterThan(RoomRightLevels.GUILD_ADMIN); + + if (!isGuildAdmin) { + GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild.getId(), habbo.getHabboInfo().getId()); + + isGuildAdmin = member != null && (member.getRank() == GuildRank.ADMIN || member.getRank() == GuildRank.OWNER); + } + + if (!isGuildAdmin) { + return false; + } + + return hasActiveMembership(habbo.getHabboInfo().getId()) && hasActiveMembership(guild.getOwnerId()); + } + + private static Guild resolvePlacementGuild(Room room) { + int guildId = resolveRoomGuildId(room); + + if (guildId <= 0) { + return null; + } + + if (room.getGuildId() != guildId) { + room.setGuild(guildId); + } + + return Emulator.getGameEnvironment().getGuildManager().getGuild(guildId); + } + + private static int resolveRoomGuildId(Room room) { + if (room == null) { + return 0; + } + + if (room.getGuildId() > 0) { + return room.getGuildId(); + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT guild_id FROM rooms WHERE id = ? LIMIT 1")) { + statement.setInt(1, room.getId()); + + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + return set.getInt("guild_id"); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception resolving Builders Club room guild", e); + } + + return 0; + } + + public static void trackPlacedItem(int itemId, int userId, int roomId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO builders_club_items (item_id, user_id, room_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE user_id = VALUES(user_id), room_id = VALUES(room_id)")) { + statement.setInt(1, itemId); + statement.setInt(2, userId); + statement.setInt(3, roomId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Caught SQL exception tracking Builders Club item placement", e); + } + } + + public static void clearTrackedItemRoom(int itemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("UPDATE builders_club_items SET room_id = 0 WHERE item_id = ? LIMIT 1")) { + statement.setInt(1, itemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Caught SQL exception clearing Builders Club room assignment", e); + } + } + + public static void deleteTrackedItem(int itemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM builders_club_items WHERE item_id = ? LIMIT 1")) { + statement.setInt(1, itemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Caught SQL exception deleting Builders Club tracked item", e); + } + } + + public static SyncResult syncRoom(Room room) { + if (room == null) { + return SyncResult.UNCHANGED; + } + + boolean hasTrackedItems = roomHasTrackedItems(room.getId()); + boolean hasMembership = hasActiveMembership(room.getOwnerId()); + + if (hasTrackedItems && !hasMembership) { + return lockRoom(room) ? SyncResult.LOCKED : SyncResult.UNCHANGED; + } + + if (room.isBuildersClubTrialLocked() && (!hasTrackedItems || hasMembership)) { + return unlockRoom(room) ? SyncResult.UNLOCKED : SyncResult.UNCHANGED; + } + + return SyncResult.UNCHANGED; + } + + public static int syncOwnedRooms(int ownerId) { + int changed = 0; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT id FROM rooms WHERE owner_id = ?")) { + statement.setInt(1, ownerId); + + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(set.getInt("id"), false); + + if (syncRoom(room) != SyncResult.UNCHANGED) { + changed++; + } + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception syncing Builders Club rooms", e); + } + + return changed; + } + + public static void sendRoomLockedBubble(int ownerId) { + sendBubbleNotification(ownerId, BubbleAlertKeys.BUILDERS_CLUB_ROOM_LOCKED, null); + } + + public static void sendRoomUnlockedBubble(int ownerId) { + sendBubbleNotification(ownerId, BubbleAlertKeys.BUILDERS_CLUB_ROOM_UNLOCKED, null); + } + + public static void sendMembershipMadeBubble(int userId) { + sendBubbleNotification(userId, BubbleAlertKeys.BUILDERS_CLUB_MEMBERSHIP_MADE, null); + } + + public static void sendMembershipExtendedBubble(int userId) { + sendBubbleNotification(userId, BubbleAlertKeys.BUILDERS_CLUB_MEMBERSHIP_EXTENDED, null); + } + + public static void sendVisitDeniedOwnerBubble(int ownerId, String username) { + THashMap keys = new THashMap<>(); + keys.put("USERNAME", username); + + sendBubbleNotification(ownerId, BubbleAlertKeys.BUILDERS_CLUB_VISIT_DENIED_OWNER, keys); + } + + public static void sendVisitDeniedVisitorAlert(int userId) { + sendSimpleAlert(userId, "notification.builders_club.visit_denied_for_visitor.message"); + } + + public static void sendMembershipExpiringAlert(int userId) { + sendSimpleAlert(userId, "expiring.bc.membership.description"); + } + + public static void sendMembershipExpiredAlert(int userId, boolean hasTrackedRooms) { + sendSimpleAlert( + userId, + hasTrackedRooms + ? "notification.builders_club.membership_expired.message" + : "notification.builders_club.membership_expired.message_no_rooms" + ); + } + + private static void sendBubbleNotification(int userId, BubbleAlertKeys key, THashMap keys) { + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (habbo == null || habbo.getClient() == null) { + return; + } + + if (keys == null) { + habbo.getClient().sendResponse(new BubbleAlertComposer(key.key)); + return; + } + + habbo.getClient().sendResponse(new BubbleAlertComposer(key.key, keys)); + } + + private static void sendSimpleAlert(int userId, String messageKey) { + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (habbo == null || habbo.getClient() == null) { + return; + } + + habbo.getClient().sendResponse(new SimpleAlertComposer(messageKey)); + } + + private static boolean lockRoom(Room room) { + if (room.isBuildersClubTrialLocked()) { + if (room.getState() != RoomState.INVISIBLE) { + room.setState(RoomState.INVISIBLE); + room.setNeedsUpdate(true); + room.save(); + } + + return false; + } + + room.setBuildersClubOriginalState(room.getState()); + room.setBuildersClubTrialLocked(true); + room.setState(RoomState.INVISIBLE); + room.setNeedsUpdate(true); + room.save(); + + return true; + } + + private static boolean unlockRoom(Room room) { + if (!room.isBuildersClubTrialLocked()) { + return false; + } + + RoomState originalState = room.getBuildersClubOriginalState(); + + if (originalState == null) { + originalState = RoomState.OPEN; + } + + room.setState(originalState); + room.setBuildersClubTrialLocked(false); + room.setBuildersClubOriginalState(originalState); + room.setNeedsUpdate(true); + room.save(); + + return true; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java index 01e81ce6..06089a0d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java @@ -5,16 +5,21 @@ import com.eu.habbo.habbohotel.bots.Bot; import com.eu.habbo.habbohotel.games.Game; import com.eu.habbo.habbohotel.guilds.Guild; import com.eu.habbo.habbohotel.guilds.GuildMember; +import com.eu.habbo.habbohotel.guilds.GuildRank; import com.eu.habbo.habbohotel.items.FurnitureType; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.*; import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameTimer; +import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameUpCounter; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.pets.Pet; import com.eu.habbo.habbohotel.pets.PetManager; import com.eu.habbo.habbohotel.users.DanceType; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredUserActionType; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredMovementPhysics; import com.eu.habbo.messages.ISerialize; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.guilds.GuildInfoComposer; @@ -24,7 +29,9 @@ import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer; import com.eu.habbo.messages.outgoing.rooms.HideDoorbellComposer; import com.eu.habbo.messages.outgoing.rooms.UpdateStackHeightComposer; import com.eu.habbo.messages.outgoing.rooms.items.*; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserIgnoredComposer; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer; +import com.eu.habbo.messages.outgoing.wired.WiredRoomSettingsDataComposer; import com.eu.habbo.plugin.Event; import com.eu.habbo.plugin.events.furniture.FurniturePickedUpEvent; import com.eu.habbo.plugin.events.rooms.RoomLoadedEvent; @@ -70,6 +77,9 @@ public class Room implements Comparable, ISerialize, Runnable { private RoomRollerManager rollerManager; private RoomMessagingManager messagingManager; private RoomCycleManager cycleManager; + private RoomUserVariableManager userVariableManager; + private RoomFurniVariableManager furniVariableManager; + private RoomVariableManager roomVariableManager; public static final Comparator SORT_SCORE = (o1, o2) -> o2.getScore() - o1.getScore(); public static final Comparator SORT_ID = (o1, o2) -> o2.getId() - o1.getId(); @@ -87,6 +97,14 @@ public class Room implements Comparable, ISerialize, Runnable { public static int ROLLERS_MAXIMUM_ROLL_AVATARS = 1; public static boolean MUTEAREA_CAN_WHISPER = false; public static double MAXIMUM_FURNI_HEIGHT = 40d; + public static final int WIRED_ACCESS_EVERYONE = 1; + public static final int WIRED_ACCESS_USERS_WITH_RIGHTS = 2; + public static final int WIRED_ACCESS_GROUP_MEMBERS = 4; + public static final int WIRED_ACCESS_GROUP_ADMINS = 8; + public static final int WIRED_ACCESS_ALLOWED_INSPECT_MASK = WIRED_ACCESS_EVERYONE | WIRED_ACCESS_USERS_WITH_RIGHTS | WIRED_ACCESS_GROUP_MEMBERS | WIRED_ACCESS_GROUP_ADMINS; + public static final int WIRED_ACCESS_ALLOWED_MODIFY_MASK = WIRED_ACCESS_USERS_WITH_RIGHTS | WIRED_ACCESS_GROUP_MEMBERS | WIRED_ACCESS_GROUP_ADMINS; + public static final int WIRED_ACCESS_DEFAULT_INSPECT_MASK = 0; + public static final int WIRED_ACCESS_DEFAULT_MODIFY_MASK = 0; static { for (int i = 1; i <= 3; i++) { @@ -126,8 +144,8 @@ public class Room implements Comparable, ISerialize, Runnable { private String password; private RoomState state; private int usersMax; - private volatile int score; - private volatile int category; + private int score; + private int category; private String floorPaint; private String wallPaint; private String backgroundPaint; @@ -136,41 +154,72 @@ public class Room implements Comparable, ISerialize, Runnable { private int floorSize; private int guild; private String tags; - private volatile boolean publicRoom; - private volatile boolean staffPromotedRoom; - private volatile boolean allowPets; - private volatile boolean allowPetsEat; - private volatile boolean allowWalkthrough; - private volatile boolean allowBotsWalk; - private volatile boolean allowEffects; - private volatile boolean hideWall; - private volatile int chatMode; - private volatile int chatWeight; - private volatile int chatSpeed; - private volatile int chatDistance; - private volatile int chatProtection; - private volatile int muteOption; - private volatile int kickOption; - private volatile int banOption; - private volatile int pollId; - private volatile boolean promoted; - private volatile int tradeMode; - private volatile boolean moveDiagonally; - private volatile boolean allowUnderpass; - private volatile boolean jukeboxActive; - private volatile boolean hideWired; + private boolean publicRoom; + private boolean staffPromotedRoom; + private boolean allowPets; + private boolean allowPetsEat; + private boolean allowWalkthrough; + private boolean allowBotsWalk; + private boolean allowEffects; + private boolean hideWall; + private int chatMode; + private int chatWeight; + private int chatSpeed; + private int chatDistance; + private int chatProtection; + private int muteOption; + private int kickOption; + private int banOption; + private int pollId; + private boolean promoted; + private int tradeMode; + private boolean moveDiagonally; + private boolean allowUnderpass; + private boolean jukeboxActive; + private boolean hideWired; + private boolean buildersClubTrialLocked; + private RoomState buildersClubOriginalState; private RoomPromotion promotion; private volatile boolean needsUpdate; private volatile boolean loaded; private volatile boolean preLoaded; private volatile boolean loadingInProgress; private volatile CompletableFuture loadingFuture; - private volatile int rollerSpeed; - private volatile int lastTimerReset = Emulator.getIntUnixTimestamp(); + private int rollerSpeed; + private int lastTimerReset = Emulator.getIntUnixTimestamp(); private volatile boolean muted; private RoomSpecialTypes roomSpecialTypes; private TraxManager traxManager; - + private final Object wiredSettingsLock = new Object(); + private volatile boolean wiredSettingsLoaded; + private int wiredInspectMask = WIRED_ACCESS_DEFAULT_INSPECT_MASK; + private int wiredModifyMask = WIRED_ACCESS_DEFAULT_MODIFY_MASK; + private boolean youtubeEnabled = false; + private String youtubeCurrentVideo = ""; + private String youtubeSenderName = ""; + private final java.util.List youtubePlaylist = new java.util.concurrent.CopyOnWriteArrayList<>(); + private final java.util.Set youtubeWatchers = java.util.concurrent.ConcurrentHashMap.newKeySet(); + + public boolean isYoutubeEnabled() { return this.youtubeEnabled; } + public void setYoutubeEnabled(boolean enabled) { this.youtubeEnabled = enabled; } + public String getYoutubeCurrentVideo() { return this.youtubeCurrentVideo; } + public String getYoutubeSenderName() { return this.youtubeSenderName; } + public java.util.List getYoutubePlaylist() { return this.youtubePlaylist; } + public java.util.Set getYoutubeWatchers() { return this.youtubeWatchers; } + + public void setYoutubeVideo(String videoId, String senderName, java.util.List playlist) { + this.youtubeCurrentVideo = videoId; + this.youtubeSenderName = senderName; + this.youtubePlaylist.clear(); + if (playlist != null) this.youtubePlaylist.addAll(playlist); + } + + public void clearYoutubeVideo() { + this.youtubeCurrentVideo = ""; + this.youtubeSenderName = ""; + this.youtubePlaylist.clear(); + } + public final THashMap cache; public Room(ResultSet set) throws SQLException { @@ -198,6 +247,7 @@ public class Room implements Comparable, ISerialize, Runnable { this.allowPetsEat = set.getBoolean("allow_other_pets_eat"); this.allowWalkthrough = set.getBoolean("allow_walkthrough"); this.hideWall = set.getBoolean("allow_hidewall"); + try { this.youtubeEnabled = set.getBoolean("youtube_enabled"); } catch (Exception e) { this.youtubeEnabled = false; } this.chatMode = set.getInt("chat_mode"); this.chatWeight = set.getInt("chat_weight"); this.chatSpeed = set.getInt("chat_speed"); @@ -214,25 +264,24 @@ public class Room implements Comparable, ISerialize, Runnable { this.promoted = set.getString("promoted").equals("1"); this.jukeboxActive = set.getString("jukebox_active").equals("1"); this.hideWired = set.getString("hidewired").equals("1"); + this.buildersClubTrialLocked = set.getBoolean("builders_club_trial_locked"); + + String buildersClubOriginalState = set.getString("builders_club_original_state"); + + if (buildersClubOriginalState != null && !buildersClubOriginalState.isEmpty()) { + try { + this.buildersClubOriginalState = RoomState.valueOf(buildersClubOriginalState.toUpperCase()); + } catch (IllegalArgumentException e) { + this.buildersClubOriginalState = RoomState.OPEN; + } + } else { + this.buildersClubOriginalState = RoomState.OPEN; + } this.bannedHabbos = new TIntObjectHashMap<>(); - try (Connection connection = Emulator.getDatabase().getDataSource() - .getConnection(); PreparedStatement statement = connection.prepareStatement( - "SELECT * FROM room_promotions WHERE room_id = ? AND end_timestamp > ? LIMIT 1")) { - if (this.promoted) { - statement.setInt(1, this.id); - statement.setInt(2, Emulator.getIntUnixTimestamp()); - - try (ResultSet promotionSet = statement.executeQuery()) { - this.promoted = false; - if (promotionSet.next()) { - this.promoted = true; - this.promotion = new RoomPromotion(this, promotionSet); - } - } - } - + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + // Load bans eagerly (needed for entry check before loadData) this.loadBans(connection); } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); @@ -278,6 +327,9 @@ public class Room implements Comparable, ISerialize, Runnable { this.rollerManager = new RoomRollerManager(this); this.messagingManager = new RoomMessagingManager(this); this.cycleManager = new RoomCycleManager(this); + this.userVariableManager = new RoomUserVariableManager(this); + this.furniVariableManager = new RoomFurniVariableManager(this); + this.roomVariableManager = new RoomVariableManager(this); } // ==================== MANAGER GETTERS ==================== @@ -359,6 +411,18 @@ public class Room implements Comparable, ISerialize, Runnable { return this.cycleManager; } + public RoomUserVariableManager getUserVariableManager() { + return this.userVariableManager; + } + + public RoomFurniVariableManager getFurniVariableManager() { + return this.furniVariableManager; + } + + public RoomVariableManager getRoomVariableManager() { + return this.roomVariableManager; + } + /** * Gets the roller manager for this room. */ @@ -485,7 +549,26 @@ public class Room implements Comparable, ISerialize, Runnable { LOGGER.error("Caught exception loading layout", e); } - // Phase 2: Load items and rights in parallel (independent operations) + if (this.promoted) { + CompletableFuture.runAsync(() -> { + try (Connection promoConnection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement stmt = promoConnection.prepareStatement( + "SELECT * FROM room_promotions WHERE room_id = ? AND end_timestamp > ? LIMIT 1")) { + stmt.setInt(1, this.id); + stmt.setInt(2, Emulator.getIntUnixTimestamp()); + try (ResultSet promoSet = stmt.executeQuery()) { + this.promoted = false; + if (promoSet.next()) { + this.promoted = true; + this.promotion = new RoomPromotion(this, promoSet); + } + } + } catch (Exception e) { + LOGGER.error("Caught exception loading promotion", e); + } + }, Emulator.getThreading().getService()); + } + CompletableFuture itemsFuture = CompletableFuture.runAsync(() -> { try (Connection itemConnection = Emulator.getDatabase().getDataSource().getConnection()) { this.loadItems(itemConnection); @@ -510,21 +593,7 @@ public class Room implements Comparable, ISerialize, Runnable { } }, Emulator.getThreading().getService()); - // Wait for items to be loaded before loading wired data (wired depends on items) - try { - itemsFuture.join(); - } catch (Exception e) { - LOGGER.error("Error waiting for items to load", e); - } - - // Phase 3: Load heightmap after items are loaded (depends on items for stack heights) - try { - this.loadHeightmap(); - } catch (Exception e) { - LOGGER.error("Caught exception loading heightmap", e); - } - - // Phase 4: Load bots, pets, and wired data in parallel (all depend on layout + items) + // Bots and pets only need layout for positioning - start them now CompletableFuture botsFuture = CompletableFuture.runAsync(() -> { try (Connection botsConnection = Emulator.getDatabase().getDataSource().getConnection()) { this.loadBots(botsConnection); @@ -541,6 +610,22 @@ public class Room implements Comparable, ISerialize, Runnable { } }, Emulator.getThreading().getService()); + // Wait for items (needed for heightmap + wired) + try { + itemsFuture.join(); + } catch (Exception e) { + LOGGER.error("Error waiting for items to load", e); + } + + // Phase 3: Heightmap and wired in parallel (both depend on items, not on each other) + CompletableFuture heightmapFuture = CompletableFuture.runAsync(() -> { + try { + this.loadHeightmap(); + } catch (Exception e) { + LOGGER.error("Caught exception loading heightmap", e); + } + }, Emulator.getThreading().getService()); + CompletableFuture wiredFuture = CompletableFuture.runAsync(() -> { try (Connection wiredConnection = Emulator.getDatabase().getDataSource().getConnection()) { this.loadWiredData(wiredConnection); @@ -549,9 +634,9 @@ public class Room implements Comparable, ISerialize, Runnable { } }, Emulator.getThreading().getService()); - // Wait for all parallel operations to complete + // Wait for all remaining operations try { - CompletableFuture.allOf(rightsFuture, wordFilterFuture, botsFuture, petsFuture, wiredFuture).join(); + CompletableFuture.allOf(rightsFuture, wordFilterFuture, botsFuture, petsFuture, heightmapFuture, wiredFuture).join(); } catch (Exception e) { LOGGER.error("Error waiting for parallel room data loading", e); } @@ -737,6 +822,8 @@ public class Room implements Comparable, ISerialize, Runnable { return; } + boolean trackedBuildersClubItem = BuildersClubRoomSupport.isTrackedItem(item.getId()); + if (Emulator.getPluginManager().isRegistered(FurniturePickedUpEvent.class, true)) { Event furniturePickedUpEvent = new FurniturePickedUpEvent(item, picker); Emulator.getPluginManager().fireEvent(furniturePickedUpEvent); @@ -779,9 +866,14 @@ public class Room implements Comparable, ISerialize, Runnable { this.sendComposer(new RemoveWallItemComposer(item).compose()); } + if (trackedBuildersClubItem) { + Emulator.getGameEnvironment().getItemManager().deleteItem(item); + return; + } + Habbo habbo = (picker != null && picker.getHabboInfo().getId() == item.getId() ? picker : Emulator.getGameServer().getGameClientManager().getHabbo(item.getUserId())); - if (habbo != null) { + if (!trackedBuildersClubItem && habbo != null) { habbo.getInventory().getItemsComponent().addItem(item); habbo.getClient().sendResponse(new AddHabboItemComposer(item)); habbo.getClient().sendResponse(new InventoryRefreshComposer()); @@ -896,7 +988,11 @@ public class Room implements Comparable, ISerialize, Runnable { } for (InteractionGameTimer timer : this.getRoomSpecialTypes().getGameTimers().values()) { - timer.setRunning(false); + if (timer instanceof InteractionGameUpCounter) { + ((InteractionGameUpCounter) timer).resetOnRoomUnload(this); + } else { + timer.setRunning(false); + } } for (Game game : this.games) { @@ -908,13 +1004,13 @@ public class Room implements Comparable, ISerialize, Runnable { this.itemManager.saveAllPendingItems(); + // Unregister all wired tickables for this room from the tick service + com.eu.habbo.habbohotel.wired.core.WiredManager.unregisterRoomTickables(this); + if (this.roomSpecialTypes != null) { this.roomSpecialTypes.dispose(); } - // Unregister all wired tickables for this room from the tick service - com.eu.habbo.habbohotel.wired.core.WiredManager.unregisterRoomTickables(this); - // Clear wired engine caches for this room if (com.eu.habbo.habbohotel.wired.core.WiredManager.getStackIndex() != null) { com.eu.habbo.habbohotel.wired.core.WiredManager.getStackIndex().invalidateAll(this); @@ -922,6 +1018,8 @@ public class Room implements Comparable, ISerialize, Runnable { if (com.eu.habbo.habbohotel.wired.core.WiredManager.getEngine() != null) { com.eu.habbo.habbohotel.wired.core.WiredManager.getEngine().clearRoomRecursionDepth(this.id); com.eu.habbo.habbohotel.wired.core.WiredManager.getEngine().clearRoomRateLimiters(this.id); + com.eu.habbo.habbohotel.wired.core.WiredManager.getEngine().clearRoomBan(this.id); + com.eu.habbo.habbohotel.wired.core.WiredManager.getEngine().clearRoomDiagnostics(this.id); } this.itemManager.clear(); @@ -1079,7 +1177,7 @@ public class Room implements Comparable, ISerialize, Runnable { if (this.needsUpdate) { try (Connection connection = Emulator.getDatabase().getDataSource() .getConnection(); PreparedStatement statement = connection.prepareStatement( - "UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ? WHERE id = ?")) { + "UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ?, youtube_enabled = ? WHERE id = ?")) { statement.setString(1, this.name); statement.setString(2, this.description); statement.setString(3, this.password); @@ -1129,7 +1227,8 @@ public class Room implements Comparable, ISerialize, Runnable { statement.setString(38, this.jukeboxActive ? "1" : "0"); statement.setString(39, this.hideWired ? "1" : "0"); statement.setString(40, this.allowUnderpass ? "1" : "0"); - statement.setInt(41, this.id); + statement.setString(41, this.youtubeEnabled ? "1" : "0"); + statement.setInt(42, this.id); statement.executeUpdate(); this.needsUpdate = false; } catch (SQLException e) { @@ -1246,6 +1345,22 @@ public class Room implements Comparable, ISerialize, Runnable { this.state = state; } + public boolean isBuildersClubTrialLocked() { + return this.buildersClubTrialLocked; + } + + public void setBuildersClubTrialLocked(boolean buildersClubTrialLocked) { + this.buildersClubTrialLocked = buildersClubTrialLocked; + } + + public RoomState getBuildersClubOriginalState() { + return this.buildersClubOriginalState; + } + + public void setBuildersClubOriginalState(RoomState buildersClubOriginalState) { + this.buildersClubOriginalState = buildersClubOriginalState; + } + public int getUsersMax() { return this.usersMax; } @@ -1345,11 +1460,28 @@ public class Room implements Comparable, ISerialize, Runnable { } public int getGuildId() { + if (this.guild > 0) { + return this.guild; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT guild_id FROM rooms WHERE id = ? LIMIT 1")) { + statement.setInt(1, this.id); + + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + this.guild = set.getInt("guild_id"); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception resolving room guild", e); + } + return this.guild; } public boolean hasGuild() { - return this.guild != 0; + return this.getGuildId() != 0; } public void setGuild(int guild) { @@ -1758,13 +1890,31 @@ public class Room implements Comparable, ISerialize, Runnable { } public void removeHabbo(Habbo habbo) { + this.cleanupYoutubeWatcher(habbo); this.unitManager.removeHabbo(habbo); } public void removeHabbo(Habbo habbo, boolean sendRemovePacket) { + this.cleanupYoutubeWatcher(habbo); this.unitManager.removeHabbo(habbo, sendRemovePacket); } + private void cleanupYoutubeWatcher(Habbo habbo) { + if (habbo == null) return; + int userId = habbo.getHabboInfo().getId(); + + // If the broadcast sender leaves, stop the broadcast for everyone + if (!this.youtubeCurrentVideo.isEmpty() + && habbo.getHabboInfo().getUsername().equals(this.youtubeSenderName)) { + this.clearYoutubeVideo(); + this.sendComposer(new com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomBroadcastComposer("", "", java.util.Collections.emptyList()).compose()); + } + + if (this.youtubeWatchers.remove(userId)) { + this.sendComposer(new com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomWatchersComposer(this.youtubeWatchers).compose()); + } + } + public void addBot(Bot bot) { this.unitManager.addBot(bot); } @@ -1874,11 +2024,17 @@ public class Room implements Comparable, ISerialize, Runnable { } public void muteHabbo(Habbo habbo, int minutes) { - this.rightsManager.muteHabbo(habbo, minutes); + this.chatManager.muteHabbo(habbo, minutes); + this.sendComposer(new RoomUserIgnoredComposer(habbo, RoomUserIgnoredComposer.MUTED).compose()); + } + + public void unmuteHabbo(Habbo habbo) { + this.chatManager.unmuteHabbo(habbo); + this.sendComposer(new RoomUserIgnoredComposer(habbo, RoomUserIgnoredComposer.UNIGNORED).compose()); } public boolean isMuted(Habbo habbo) { - return this.rightsManager.isMuted(habbo); + return this.chatManager.isMuted(habbo); } public void habboEntered(Habbo habbo) { @@ -2020,6 +2176,10 @@ public class Room implements Comparable, ISerialize, Runnable { this.messagingManager.sendComposer(message); } + public void sendComposers(Collection messages) { + this.messagingManager.sendComposers(messages); + } + public void sendComposerToHabbosWithRights(ServerMessage message) { this.messagingManager.sendComposerToHabbosWithRights(message); } @@ -2069,11 +2229,18 @@ public class Room implements Comparable, ISerialize, Runnable { } public RoomRightLevels getGuildRightLevel(Habbo habbo) { - if (this.guild > 0 && habbo.getHabboStats().hasGuild(this.guild)) { - Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(this.guild); + int guildId = this.getGuildId(); - if (Emulator.getGameEnvironment().getGuildManager().getOnlyAdmins(guild) - .get(habbo.getHabboInfo().getId()) != null) { + if (guildId > 0 && habbo != null && habbo.getHabboInfo() != null) { + Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId); + + if (guild == null) { + return RoomRightLevels.NONE; + } + + GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild.getId(), habbo.getHabboInfo().getId()); + + if ((member != null) && (member.getRank() == GuildRank.ADMIN || member.getRank() == GuildRank.OWNER)) { return RoomRightLevels.GUILD_ADMIN; } @@ -2101,20 +2268,108 @@ public class Room implements Comparable, ISerialize, Runnable { return this.rightsManager.hasRights(habbo); } + public boolean hasExplicitRights(Habbo habbo) { + return habbo != null && this.rights.contains(habbo.getHabboInfo().getId()); + } + + public int getWiredInspectMask() { + this.ensureWiredSettingsLoaded(); + return this.wiredInspectMask; + } + + public int getWiredModifyMask() { + this.ensureWiredSettingsLoaded(); + return this.wiredModifyMask; + } + + public boolean canInspectWired(Habbo habbo) { + if (habbo == null) { + return false; + } + + if (this.canManageWiredSettings(habbo)) { + return true; + } + + this.ensureWiredSettingsLoaded(); + return this.matchesWiredAccessMask(habbo, this.wiredInspectMask, true); + } + + public boolean canModifyWired(Habbo habbo) { + if (habbo == null) { + return false; + } + + if (this.canManageWiredSettings(habbo)) { + return true; + } + + this.ensureWiredSettingsLoaded(); + return this.matchesWiredAccessMask(habbo, this.wiredModifyMask, false); + } + + public boolean canManageWiredSettings(Habbo habbo) { + return habbo != null && this.isOwner(habbo); + } + + public boolean saveWiredSettings(int inspectMask, int modifyMask) { + int sanitizedInspectMask = sanitizeWiredInspectMask(inspectMask); + int sanitizedModifyMask = sanitizeWiredModifyMask(modifyMask); + sanitizedInspectMask |= sanitizedModifyMask; + + synchronized (this.wiredSettingsLock) { + int previousInspectMask = this.wiredInspectMask; + int previousModifyMask = this.wiredModifyMask; + this.wiredInspectMask = sanitizedInspectMask; + this.wiredModifyMask = sanitizedModifyMask; + this.wiredSettingsLoaded = true; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO room_wired_settings (room_id, inspect_mask, modify_mask) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE inspect_mask = VALUES(inspect_mask), modify_mask = VALUES(modify_mask)")) { + statement.setInt(1, this.id); + statement.setInt(2, sanitizedInspectMask); + statement.setInt(3, sanitizedModifyMask); + statement.executeUpdate(); + this.pushWiredSettingsToCurrentHabbos(); + return true; + } catch (SQLException e) { + this.wiredInspectMask = previousInspectMask; + this.wiredModifyMask = previousModifyMask; + LOGGER.error("Caught SQL exception while saving wired room settings", e); + return false; + } + } + } + public void giveRights(Habbo habbo) { - this.rightsManager.giveRights(habbo); + if (habbo == null) { + return; + } + + this.giveRights(habbo.getHabboInfo().getId()); } public void giveRights(int userId) { this.rightsManager.giveRights(userId); + + if (!this.rights.contains(userId)) { + this.rights.add(userId); + } + + this.pushWiredSettingsToCurrentHabbos(); } public void removeRights(int userId) { this.rightsManager.removeRights(userId); + this.rights.remove(userId); + this.pushWiredSettingsToCurrentHabbos(); } public void removeAllRights() { this.rightsManager.removeAllRights(); + this.rights.clear(); + this.pushWiredSettingsToCurrentHabbos(); } void refreshRightsInRoom() { @@ -2125,7 +2380,7 @@ public class Room implements Comparable, ISerialize, Runnable { this.rightsManager.refreshRightsForHabbo(habbo); } - public THashMap getUsersWithRights() { + public Map getUsersWithRights() { return this.rightsManager.getUsersWithRights(); } @@ -2141,6 +2396,111 @@ public class Room implements Comparable, ISerialize, Runnable { return this.bannedHabbos; } + private void ensureWiredSettingsLoaded() { + if (this.wiredSettingsLoaded) { + return; + } + + synchronized (this.wiredSettingsLock) { + if (this.wiredSettingsLoaded) { + return; + } + + this.wiredInspectMask = WIRED_ACCESS_DEFAULT_INSPECT_MASK; + this.wiredModifyMask = WIRED_ACCESS_DEFAULT_MODIFY_MASK; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT inspect_mask, modify_mask FROM room_wired_settings WHERE room_id = ? LIMIT 1")) { + statement.setInt(1, this.id); + + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + this.wiredInspectMask = sanitizeWiredInspectMask(set.getInt("inspect_mask")); + this.wiredModifyMask = sanitizeWiredModifyMask(set.getInt("modify_mask")); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception while loading wired room settings", e); + } + + this.wiredSettingsLoaded = true; + } + } + + private boolean matchesWiredAccessMask(Habbo habbo, int mask, boolean allowEveryone) { + if (habbo == null) { + return false; + } + + if (allowEveryone && hasWiredAccess(mask, WIRED_ACCESS_EVERYONE)) { + return true; + } + + if (hasWiredAccess(mask, WIRED_ACCESS_USERS_WITH_RIGHTS) && this.hasExplicitRights(habbo)) { + return true; + } + + if (hasWiredAccess(mask, WIRED_ACCESS_GROUP_ADMINS) && this.isRoomGroupAdmin(habbo)) { + return true; + } + + return hasWiredAccess(mask, WIRED_ACCESS_GROUP_MEMBERS) && this.isRoomGroupMember(habbo); + } + + private boolean isRoomGroupMember(Habbo habbo) { + return habbo != null && this.guild > 0 && habbo.getHabboStats().hasGuild(this.guild); + } + + private boolean isRoomGroupAdmin(Habbo habbo) { + if (!this.isRoomGroupMember(habbo)) { + return false; + } + + GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(this.guild, habbo.getHabboInfo().getId()); + + if (member == null) { + return false; + } + + GuildRank rank = member.getRank(); + return rank == GuildRank.OWNER || rank == GuildRank.ADMIN; + } + + private static boolean hasWiredAccess(int mask, int permissionMask) { + return (mask & permissionMask) != 0; + } + + private static int sanitizeWiredInspectMask(int mask) { + int sanitizedMask = mask & WIRED_ACCESS_ALLOWED_INSPECT_MASK; + + if (hasWiredAccess(sanitizedMask, WIRED_ACCESS_GROUP_MEMBERS)) { + sanitizedMask |= WIRED_ACCESS_GROUP_ADMINS; + } + + return sanitizedMask; + } + + private static int sanitizeWiredModifyMask(int mask) { + int sanitizedMask = mask & WIRED_ACCESS_ALLOWED_MODIFY_MASK; + + if (hasWiredAccess(sanitizedMask, WIRED_ACCESS_GROUP_MEMBERS)) { + sanitizedMask |= WIRED_ACCESS_GROUP_ADMINS; + } + + return sanitizedMask; + } + + private void pushWiredSettingsToCurrentHabbos() { + for (Habbo currentHabbo : this.getCurrentHabbos().values()) { + if (currentHabbo == null || currentHabbo.getClient() == null) { + continue; + } + + currentHabbo.getClient().sendResponse(new WiredRoomSettingsDataComposer(this, currentHabbo)); + } + } + public void addRoomBan(RoomBan roomBan) { this.rightsManager.addRoomBan(roomBan); } @@ -2162,6 +2522,7 @@ public class Room implements Comparable, ISerialize, Runnable { - habbo.getRoomUnit().getBodyRotation().getValue() % 2]); habbo.getRoomUnit().setStatus(RoomUnitStatus.SIT, 0.5 + ""); this.sendComposer(new RoomUserStatusComposer(habbo.getRoomUnit()).compose()); + WiredManager.triggerUserPerformsAction(this, habbo.getRoomUnit(), WiredUserActionType.SIT, -1); } public void makeStand(Habbo habbo) { @@ -2171,12 +2532,19 @@ public class Room implements Comparable, ISerialize, Runnable { HabboItem item = this.getTopItemAt(habbo.getRoomUnit().getX(), habbo.getRoomUnit().getY()); if (item == null || !item.getBaseItem().allowSit() || !item.getBaseItem().allowLay()) { + boolean wasSittingOrLaying = habbo.getRoomUnit().hasStatus(RoomUnitStatus.SIT) + || habbo.getRoomUnit().hasStatus(RoomUnitStatus.LAY); habbo.getRoomUnit().cmdStand = true; habbo.getRoomUnit().setBodyRotation( RoomUserRotation.values()[habbo.getRoomUnit().getBodyRotation().getValue() - habbo.getRoomUnit().getBodyRotation().getValue() % 2]); habbo.getRoomUnit().removeStatus(RoomUnitStatus.SIT); + habbo.getRoomUnit().removeStatus(RoomUnitStatus.LAY); this.sendComposer(new RoomUserStatusComposer(habbo.getRoomUnit()).compose()); + + if (wasSittingOrLaying) { + WiredManager.triggerUserPerformsAction(this, habbo.getRoomUnit(), WiredUserActionType.STAND, -1); + } } } @@ -2197,29 +2565,38 @@ public class Room implements Comparable, ISerialize, Runnable { } public void updateItem(HabboItem item) { - if (this.isLoaded()) { - if (item != null && item.getRoomId() == this.id) { - if (item.getBaseItem() != null) { - if (item.getBaseItem().getType() == FurnitureType.FLOOR) { - this.sendComposer(new FloorItemUpdateComposer(item).compose()); - this.updateTiles(this.getLayout() - .getTilesAt(this.layout.getTile(item.getX(), item.getY()), - item.getBaseItem().getWidth(), item.getBaseItem().getLength(), - item.getRotation())); - } else if (item.getBaseItem().getType() == FurnitureType.WALL) { - this.sendComposer(new WallItemUpdateComposer(item).compose()); + if (this.isLoaded()) { + if (item != null && item.getRoomId() == this.id) { + if (item.getBaseItem() != null) { + if (item.getBaseItem().getType() == FurnitureType.FLOOR) { + this.sendComposer(new FloorItemUpdateComposer(item).compose()); + this.updateTiles(this.getLayout() + .getTilesAt(this.layout.getTile(item.getX(), item.getY()), + item.getBaseItem().getWidth(), item.getBaseItem().getLength(), + item.getRotation())); + + if (RoomAreaHideSupport.isControllerItem(item)) { + RoomAreaHideSupport.sendState(this, item); + } + } else if (item.getBaseItem().getType() == FurnitureType.WALL) { + this.sendComposer(new WallItemUpdateComposer(item).compose()); + } } } - } } } public void updateItemState(HabboItem item) { - if (!item.isLimited()) { - this.sendComposer(new ItemStateComposer(item).compose()); - } else { - this.sendComposer(new FloorItemUpdateComposer(item).compose()); - } + if (item != null && RoomAreaHideSupport.isControllerItem(item)) { + this.updateItem(item); + return; + } + + if (!item.isLimited()) { + this.sendComposer(new ItemStateComposer(item).compose()); + } else { + this.sendComposer(new FloorItemUpdateComposer(item).compose()); + } if (item.getBaseItem().getType() == FurnitureType.FLOOR) { if (this.layout == null) { @@ -2234,6 +2611,16 @@ public class Room implements Comparable, ISerialize, Runnable { ((InteractionMultiHeight) item).updateUnitsOnItem(this); } } + + if (item.getBaseItem().getType() == FurnitureType.FLOOR + && (RoomConfInvisSupport.isControllerItem(item) || RoomConfInvisSupport.isTarget(item))) { + RoomConfInvisSupport.sendState(this); + } + + if (item.getBaseItem().getType() == FurnitureType.FLOOR + && RoomHanditemBlockSupport.isControllerItem(item)) { + RoomHanditemBlockSupport.sendState(this); + } } public int getUserFurniCount(int userId) { @@ -2421,6 +2808,11 @@ public class Room implements Comparable, ISerialize, Runnable { return this.itemManager.furnitureFitsAt(tile, item, rotation, checkForUnits); } + public FurnitureMovementError furnitureFitsAtWithPhysics(RoomTile tile, HabboItem item, int rotation, + boolean checkForUnits, WiredMovementPhysics physics) { + return this.itemManager.furnitureFitsAtWithPhysics(tile, item, rotation, checkForUnits, physics); + } + public FurnitureMovementError placeFloorFurniAt(HabboItem item, RoomTile tile, int rotation, Habbo owner) { return this.itemManager.placeFloorFurniAt(item, tile, rotation, owner); @@ -2457,6 +2849,16 @@ public class Room implements Comparable, ISerialize, Runnable { return this.itemManager.moveFurniTo(item, tile, rotation, z, actor, sendUpdates, checkForUnits); } + public FurnitureMovementError moveFurniToWithPhysics(HabboItem item, RoomTile tile, int rotation, + Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) { + return this.itemManager.moveFurniToWithPhysics(item, tile, rotation, actor, sendUpdates, checkForUnits, physics); + } + + public FurnitureMovementError moveFurniToWithPhysics(HabboItem item, RoomTile tile, int rotation, double z, + Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) { + return this.itemManager.moveFurniToWithPhysics(item, tile, rotation, z, actor, sendUpdates, checkForUnits, physics); + } + public FurnitureMovementError slideFurniTo(HabboItem item, RoomTile tile, int rotation) { return this.itemManager.slideFurniTo(item, tile, rotation); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomAreaHideSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomAreaHideSupport.java new file mode 100644 index 00000000..426cbf4c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomAreaHideSupport.java @@ -0,0 +1,100 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.interactions.InteractionAreaHideControl; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.outgoing.rooms.items.AreaHideComposer; + +public final class RoomAreaHideSupport { + private RoomAreaHideSupport() { + } + + public static boolean isControllerItem(HabboItem item) { + return item instanceof InteractionAreaHideControl + || hasInteractionName(item, "wf_conf_area_hide") + || hasInteractionName(item, "conf_area_hide"); + } + + public static boolean isControllerActive(HabboItem item) { + return isControllerItem(item) && getState(item) == 1; + } + + public static int getState(HabboItem item) { + return Math.min(1, readIntValue(item, "state", 0)); + } + + public static int getRootX(HabboItem item) { + return readIntValue(item, "rootX", 0); + } + + public static int getRootY(HabboItem item) { + return readIntValue(item, "rootY", 0); + } + + public static int getWidth(HabboItem item) { + return readIntValue(item, "width", 0); + } + + public static int getLength(HabboItem item) { + return readIntValue(item, "length", 0); + } + + public static boolean isInvisibilityEnabled(HabboItem item) { + return readIntValue(item, "invisibility", 0) == 1; + } + + public static boolean includesWallItems(HabboItem item) { + return readIntValue(item, "wallItems", 0) == 1; + } + + public static boolean isInverted(HabboItem item) { + return readIntValue(item, "invert", 0) == 1; + } + + public static void sendState(Room room, HabboItem item) { + if (room == null || item == null || !isControllerItem(item)) { + return; + } + + room.sendComposer(new AreaHideComposer(item).compose()); + } + + public static void sendState(Room room, GameClient client) { + if (room == null || client == null) { + return; + } + + for (HabboItem item : room.getFloorItems()) { + if (!isControllerActive(item)) { + continue; + } + + client.sendResponse(new AreaHideComposer(item).compose()); + } + } + + private static int readIntValue(HabboItem item, String key, int fallback) { + if (!(item instanceof InteractionAreaHideControl) || key == null) { + return fallback; + } + + InteractionAreaHideControl areaHide = (InteractionAreaHideControl) item; + String value = areaHide.values.get(key); + + try { + return Math.max(0, Integer.parseInt(value)); + } catch (Exception ignored) { + return fallback; + } + } + + private static boolean hasInteractionName(HabboItem item, String interactionName) { + return item != null + && item.getBaseItem() != null + && item.getBaseItem().getType() == FurnitureType.FLOOR + && item.getBaseItem().getInteractionType() != null + && item.getBaseItem().getInteractionType().getName() != null + && item.getBaseItem().getInteractionType().getName().equalsIgnoreCase(interactionName); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatManager.java index 3665aca1..4b52804a 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatManager.java @@ -156,6 +156,15 @@ public class RoomChatManager { } } + /** + * Removes a room mute from a Habbo. + */ + public void unmuteHabbo(Habbo habbo) { + synchronized (this.mutedHabbos) { + this.mutedHabbos.remove(habbo.getHabboInfo().getId()); + } + } + /** * Checks if a Habbo is muted. */ @@ -183,7 +192,8 @@ public class RoomChatManager { */ public int getMuteTimeRemaining(Habbo habbo) { if (this.mutedHabbos.containsKey(habbo.getHabboInfo().getId())) { - return this.mutedHabbos.get(habbo.getHabboInfo().getId()) - Emulator.getIntUnixTimestamp(); + return Math.max(0, + this.mutedHabbos.get(habbo.getHabboInfo().getId()) - Emulator.getIntUnixTimestamp()); } return 0; } @@ -298,26 +308,29 @@ public class RoomChatManager { if (this.isMuted(habbo)) { habbo.getClient().sendResponse(new MutedWhisperComposer( - this.mutedHabbos.get(habbo.getHabboInfo().getId()) - Emulator.getIntUnixTimestamp())); + Math.max(1, this.getMuteTimeRemaining(habbo)))); return; } } + String wiredSayMessage = roomChatMessage.getMessage(); + // Handle commands and wired + boolean suppressSaysOutput = false; if (chatType != RoomChatType.WHISPER) { if (CommandHandler.handleCommand(habbo.getClient(), roomChatMessage.getUnfilteredMessage())) { - WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), roomChatMessage.getMessage()); + WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), wiredSayMessage); roomChatMessage.isCommand = true; return; } if (!ignoreWired) { - if (WiredManager.triggerUserSays(habbo.getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), roomChatMessage.getMessage())) { - habbo.getClient().sendResponse(new RoomUserWhisperComposer( - new RoomChatMessage(roomChatMessage.getMessage(), habbo, habbo, - roomChatMessage.getBubble()))); - return; - } + suppressSaysOutput = WiredManager.shouldSuppressUserSaysOutput( + habbo.getHabboInfo().getCurrentRoom(), + habbo.getRoomUnit(), + wiredSayMessage, + chatType.ordinal(), + roomChatMessage.getBubble() != null ? roomChatMessage.getBubble().getType() : -1); } } @@ -378,9 +391,30 @@ public class RoomChatManager { if (chatType == RoomChatType.WHISPER) { this.handleWhisper(habbo, roomChatMessage, prefixMessage, clearPrefixMessage); } else if (chatType == RoomChatType.TALK) { - this.handleTalk(habbo, roomChatMessage, prefixMessage, clearPrefixMessage, tentRectangle); + if (suppressSaysOutput) { + habbo.getClient().sendResponse(new RoomUserWhisperComposer( + new RoomChatMessage(roomChatMessage.getMessage(), habbo, habbo, + roomChatMessage.getBubble()))); + } else { + this.handleTalk(habbo, roomChatMessage, prefixMessage, clearPrefixMessage, tentRectangle); + } } else if (chatType == RoomChatType.SHOUT) { - this.handleShout(habbo, roomChatMessage, prefixMessage, clearPrefixMessage, tentRectangle); + if (suppressSaysOutput) { + habbo.getClient().sendResponse(new RoomUserWhisperComposer( + new RoomChatMessage(roomChatMessage.getMessage(), habbo, habbo, + roomChatMessage.getBubble()))); + } else { + this.handleShout(habbo, roomChatMessage, prefixMessage, clearPrefixMessage, tentRectangle); + } + } + + if (chatType != RoomChatType.WHISPER && !ignoreWired && !roomChatMessage.isCommand) { + WiredManager.triggerUserSays( + habbo.getHabboInfo().getCurrentRoom(), + habbo.getRoomUnit(), + wiredSayMessage, + chatType.ordinal(), + roomChatMessage.getBubble() != null ? roomChatMessage.getBubble().getType() : -1); } // Notify bots and talking furniture diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessage.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessage.java index ebd915b7..b6f83d43 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessage.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessage.java @@ -202,6 +202,25 @@ public class RoomChatMessage implements Runnable, ISerialize, DatabaseLoggable { message.appendInt(0); message.appendString(this.RoomChatColour); //Added packet for room chat message.appendInt(this.getMessage().length()); + + // Custom prefix data + String prefixText = ""; + String prefixColor = ""; + String prefixIcon = ""; + String prefixEffect = ""; + if (this.habbo != null && this.habbo.getInventory() != null && this.habbo.getInventory().getPrefixesComponent() != null) { + com.eu.habbo.habbohotel.users.UserPrefix activePrefix = this.habbo.getInventory().getPrefixesComponent().getActivePrefix(); + if (activePrefix != null) { + prefixText = activePrefix.getText(); + prefixColor = activePrefix.getColor(); + prefixIcon = activePrefix.getIcon(); + prefixEffect = activePrefix.getEffect(); + } + } + message.appendString(prefixText); + message.appendString(prefixColor); + message.appendString(prefixIcon); + message.appendString(prefixEffect); } catch (Exception e) { LOGGER.error("Caught exception", e); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessageBubbles.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessageBubbles.java index 05093e8e..d38133e4 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessageBubbles.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomChatMessageBubbles.java @@ -52,14 +52,14 @@ public class RoomChatMessageBubbles { public static final RoomChatMessageBubbles UNKNOWN_43 = new RoomChatMessageBubbles(43, "UNKNOWN_43", "", true, false); public static final RoomChatMessageBubbles UNKNOWN_44 = new RoomChatMessageBubbles(44, "UNKNOWN_44", "", true, false); public static final RoomChatMessageBubbles UNKNOWN_45 = new RoomChatMessageBubbles(45, "UNKNOWN_45", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_46 = new RoomChatMessageBubbles(45, "UNKNOWN_46", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_47 = new RoomChatMessageBubbles(45, "UNKNOWN_47", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_48 = new RoomChatMessageBubbles(45, "UNKNOWN_48", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_49 = new RoomChatMessageBubbles(45, "UNKNOWN_49", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_50 = new RoomChatMessageBubbles(45, "UNKNOWN_50", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_51 = new RoomChatMessageBubbles(45, "UNKNOWN_51", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_52 = new RoomChatMessageBubbles(45, "UNKNOWN_52", "", true, false); - public static final RoomChatMessageBubbles UNKNOWN_53 = new RoomChatMessageBubbles(45, "UNKNOWN_53", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_46 = new RoomChatMessageBubbles(46, "UNKNOWN_46", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_47 = new RoomChatMessageBubbles(47, "UNKNOWN_47", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_48 = new RoomChatMessageBubbles(48, "UNKNOWN_48", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_49 = new RoomChatMessageBubbles(49, "UNKNOWN_49", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_50 = new RoomChatMessageBubbles(50, "UNKNOWN_50", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_51 = new RoomChatMessageBubbles(51, "UNKNOWN_51", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_52 = new RoomChatMessageBubbles(52, "UNKNOWN_52", "", true, false); + public static final RoomChatMessageBubbles UNKNOWN_53 = new RoomChatMessageBubbles(53, "UNKNOWN_53", "", true, false); static { @@ -167,11 +167,11 @@ public class RoomChatMessageBubbles { public static void removeDynamicBubbles() { synchronized (BUBBLES) { - BUBBLES.entrySet().removeIf(entry -> entry.getKey() > 45); + BUBBLES.entrySet().removeIf(entry -> entry.getKey() > 53); } } public static RoomChatMessageBubbles[] values() { return BUBBLES.values().toArray(new RoomChatMessageBubbles[0]); } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomConfInvisSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomConfInvisSupport.java new file mode 100644 index 00000000..53b5be41 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomConfInvisSupport.java @@ -0,0 +1,107 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.interactions.InteractionConfInvisControl; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.outgoing.rooms.items.ConfInvisStateComposer; +import gnu.trove.list.array.TIntArrayList; + +import java.util.regex.Pattern; + +public final class RoomConfInvisSupport { + private RoomConfInvisSupport() { + } + + public static boolean isControllerItem(HabboItem item) { + return item instanceof InteractionConfInvisControl + || hasInteractionName(item, "wf_conf_invis_control"); + } + + public static boolean isControllerActive(HabboItem item) { + return isControllerItem(item) && "1".equals(item.getExtradata()); + } + + public static boolean isTarget(HabboItem item) { + return item != null + && item.getBaseItem() != null + && item.getBaseItem().getType() == FurnitureType.FLOOR + && hasCustomParamToken(item.getBaseItem().getCustomParams(), "is_invisible"); + } + + public static TIntArrayList collectHiddenFloorItemIds(Room room) { + TIntArrayList hiddenItemIds = new TIntArrayList(); + + if (room == null) { + return hiddenItemIds; + } + + if (!hasActiveController(room)) { + return hiddenItemIds; + } + + for (HabboItem item : room.getFloorItems()) { + if (isTarget(item)) { + hiddenItemIds.add(item.getId()); + } + } + + return hiddenItemIds; + } + + public static boolean hasActiveController(Room room) { + if (room == null) { + return false; + } + + for (HabboItem item : room.getFloorItems()) { + if (isControllerActive(item)) { + return true; + } + } + + return false; + } + + public static void sendState(Room room) { + if (room == null) { + return; + } + + room.sendComposer(new ConfInvisStateComposer(room).compose()); + } + + public static void sendState(Room room, GameClient client) { + if (room == null || client == null) { + return; + } + + client.sendResponse(new ConfInvisStateComposer(room).compose()); + } + + private static boolean hasCustomParamToken(String value, String token) { + if (value == null || token == null) { + return false; + } + + String normalized = value.trim().toLowerCase(); + + if (normalized.isEmpty()) { + return false; + } + + Pattern pattern = Pattern.compile("(^|[^a-z0-9_])" + Pattern.quote(token.toLowerCase()) + "($|[^a-z0-9_])"); + + return pattern.matcher(normalized).find(); + } + + private static boolean hasInteractionName(HabboItem item, String interactionName) { + if (item == null || item.getBaseItem() == null || item.getBaseItem().getInteractionType() == null || interactionName == null) { + return false; + } + + String currentInteractionName = item.getBaseItem().getInteractionType().getName(); + + return currentInteractionName != null && currentInteractionName.equalsIgnoreCase(interactionName); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java index 33e1cde8..c3898db1 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java @@ -10,6 +10,7 @@ import com.eu.habbo.habbohotel.users.DanceType; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.rooms.RoomAccessDeniedComposer; import com.eu.habbo.messages.outgoing.rooms.users.RoomUnitIdleComposer; @@ -123,7 +124,19 @@ public class RoomCycleManager { // Send status updates if (!updatedUnit.isEmpty()) { - this.room.sendComposer(new RoomUserStatusComposer(updatedUnit, true).compose()); + ServerMessage statusComposer = new RoomUserStatusComposer(updatedUnit, true).compose(); + WiredMoveCarryHelper.beginMovementCollection(); + WiredMoveCarryHelper.processUserFollowers(this.room, updatedUnit); + ServerMessage wiredMovementsComposer = WiredMoveCarryHelper.finishMovementCollection(); + + if (wiredMovementsComposer != null) { + ArrayList batchedMessages = new ArrayList<>(2); + batchedMessages.add(statusComposer); + batchedMessages.add(wiredMovementsComposer); + this.room.sendComposers(batchedMessages); + } else { + this.room.sendComposer(statusComposer); + } } // Cycle trax manager @@ -364,7 +377,8 @@ public class RoomCycleManager { * Processes roller cycle. */ private void processRollers(THashSet updatedUnit) { - int rollerSpeed = this.room.getRollerSpeed(); + Integer controlledRollerSpeed = RoomQueueSpeedControlSupport.getEffectiveRollerSpeed(this.room); + int rollerSpeed = (controlledRollerSpeed != null) ? controlledRollerSpeed : this.room.getRollerSpeed(); if (rollerSpeed != -1 && this.rollerCycle >= rollerSpeed) { this.rollerCycle = 0; this.room.getRollerManager().processRollerCycle(updatedUnit, this.cycleTimestamp); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomFurniVariableManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomFurniVariableManager.java new file mode 100644 index 00000000..0a199b43 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomFurniVariableManager.java @@ -0,0 +1,1025 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableEcho; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFurniVariable; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredVariableLevelSystemSupport; +import com.eu.habbo.habbohotel.wired.core.WiredVariableTextConnectorSupport; +import com.eu.habbo.messages.outgoing.wired.WiredUserVariablesDataComposer; +import gnu.trove.set.hash.THashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public class RoomFurniVariableManager { + private static final Logger LOGGER = LoggerFactory.getLogger(RoomFurniVariableManager.class); + + private final Room room; + private final ConcurrentHashMap> activeAssignmentsByFurniId; + private volatile boolean permanentAssignmentsLoaded; + + public RoomFurniVariableManager(Room room) { + this.room = room; + this.activeAssignmentsByFurniId = new ConcurrentHashMap<>(); + this.permanentAssignmentsLoaded = false; + } + + public void ensurePermanentAssignmentsLoaded() { + if (this.permanentAssignmentsLoaded) { + return; + } + + synchronized (this) { + if (this.permanentAssignmentsLoaded) { + return; + } + + List staleRows = new ArrayList<>(); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT furni_id, variable_item_id, value, created_at, updated_at FROM room_furni_wired_variables WHERE room_id = ?")) { + statement.setInt(1, this.room.getId()); + + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + int furniId = set.getInt("furni_id"); + int definitionItemId = set.getInt("variable_item_id"); + HabboItem furni = this.room.getHabboItem(furniId); + WiredExtraFurniVariable definition = this.getDefinition(definitionItemId); + + if (furni == null || definition == null || !definition.isPermanentAvailability()) { + staleRows.add(new int[]{furniId, definitionItemId}); + continue; + } + + Integer value = null; + int rawValue = set.getInt("value"); + if (!set.wasNull()) { + value = rawValue; + } + + int createdAt = normalizeTimestamp(set.getInt("created_at"), 0); + int updatedAt = normalizeTimestamp(set.getInt("updated_at"), createdAt); + + this.activeAssignmentsByFurniId + .computeIfAbsent(furniId, key -> new ConcurrentHashMap<>()) + .put(definitionItemId, new VariableAssignment(value, createdAt, updatedAt)); + } + } + } catch (SQLException e) { + LOGGER.error("Failed to restore wired furni variables for room {}", this.room.getId(), e); + } + + for (int[] staleRow : staleRows) { + this.deletePersistentAssignment(staleRow[0], staleRow[1]); + } + + this.permanentAssignmentsLoaded = true; + } + } + + public boolean assignVariable(HabboItem furni, WiredExtraFurniVariable definition, Integer value, boolean overrideExisting) { + return definition != null && this.assignVariable(furni, definition.getId(), value, overrideExisting); + } + + public boolean assignVariable(HabboItem furni, int definitionItemId, Integer value, boolean overrideExisting) { + if (furni == null || definitionItemId <= 0) { + return false; + } + + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return false; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + boolean hadBefore = this.hasVariable(furni.getId(), definitionItemId); + Integer previousValue = (definitionInfo.hasValue() && hadBefore) ? this.getCurrentValue(furni.getId(), definitionItemId) : null; + + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).assignValue(this.room, furni.getId(), definitionInfo.hasValue() ? value : null, overrideExisting); + boolean shouldEmit = changed || (definitionInfo.hasValue() && hadBefore && overrideExisting && Objects.equals(previousValue, value)); + + if (shouldEmit) { + boolean hasAfter = this.hasVariable(furni.getId(), definitionItemId); + Integer currentValue = (definitionInfo.hasValue() && hasAfter) ? this.getCurrentValue(furni.getId(), definitionItemId) : null; + this.emitVariableChangedEvents(furni.getId(), extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + WiredExtraFurniVariable definition = this.getDefinition(definitionItemId); + if (definition == null) { + return false; + } + + this.ensurePermanentAssignmentsLoaded(); + + int furniId = furni.getId(); + Integer normalizedValue = definition.hasValue() ? value : null; + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.computeIfAbsent(furniId, key -> new ConcurrentHashMap<>()); + VariableAssignment existingAssignment = assignments.get(definitionItemId); + + if (existingAssignment != null && !overrideExisting) { + return false; + } + + boolean overwritten = existingAssignment != null && overrideExisting; + boolean valueChanged = existingAssignment == null || !Objects.equals(existingAssignment.getValue(), normalizedValue); + boolean changed = overwritten || valueChanged; + + if (existingAssignment == null || overwritten) { + int now = Emulator.getIntUnixTimestamp(); + assignments.put(definitionItemId, new VariableAssignment(normalizedValue, now, now)); + } else if (valueChanged) { + existingAssignment.setValue(normalizedValue, Emulator.getIntUnixTimestamp()); + } + + if (definition.isPermanentAvailability()) { + this.upsertPersistentAssignment(furniId, definitionItemId, assignments.get(definitionItemId)); + } else { + this.deletePersistentAssignment(furniId, definitionItemId); + } + + if (changed || (definitionInfo.hasValue() && hadBefore && overrideExisting && Objects.equals(previousValue, normalizedValue))) { + boolean hasAfter = this.hasVariable(furniId, definitionItemId); + Integer currentValue = (definitionInfo.hasValue() && hasAfter) ? this.getCurrentValue(furniId, definitionItemId) : null; + this.emitVariableChangedEvents(furniId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + public boolean updateVariableValue(int furniId, int definitionItemId, Integer value) { + this.ensurePermanentAssignmentsLoaded(); + + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + if (definitionInfo == null || !definitionInfo.hasValue() || definitionInfo.isReadOnly()) { + return false; + } + + boolean hadBefore = this.hasVariable(furniId, definitionItemId); + Integer previousValue = hadBefore ? this.getCurrentValue(furniId, definitionItemId) : null; + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).updateValue(this.room, furniId, value); + boolean shouldEmit = changed || (hadBefore && Objects.equals(previousValue, value)); + + if (shouldEmit) { + boolean hasAfter = this.hasVariable(furniId, definitionItemId); + Integer currentValue = hasAfter ? this.getCurrentValue(furniId, definitionItemId) : null; + this.emitVariableChangedEvents(furniId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + WiredExtraFurniVariable definition = this.getDefinition(definitionItemId); + if (definition == null) { + return false; + } + + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.get(furniId); + + if (assignments == null) { + return false; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + + if (assignment == null) { + return false; + } + + if (Objects.equals(assignment.getValue(), value)) { + this.emitVariableChangedEvents(furniId, extra, definitionInfo, true, previousValue, true, assignment.getValue()); + return false; + } + + assignment.setValue(value, Emulator.getIntUnixTimestamp()); + + if (definition.isPermanentAvailability()) { + this.upsertPersistentAssignment(furniId, definitionItemId, assignment); + } + + this.emitVariableChangedEvents(furniId, extra, definitionInfo, hadBefore, previousValue, true, assignment.getValue()); + this.broadcastSnapshot(); + return true; + } + + public int getCurrentValue(int furniId, int definitionItemId) { + this.ensurePermanentAssignmentsLoaded(); + + if (furniId <= 0 || definitionItemId <= 0) { + return 0; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definitionItemId); + if (derivedDefinition != null) { + Integer baseValue = this.getRawValue(furniId, derivedDefinition.getBaseDefinitionItemId()); + Integer derivedValue = WiredVariableLevelSystemSupport.getDerivedValue(derivedDefinition.getLevelSystem(), derivedDefinition.getSubvariableType(), baseValue); + return (derivedValue != null) ? derivedValue : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getCurrentValue(this.room, furniId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.get(furniId); + + if (assignments == null) { + return 0; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + + if (assignment == null || assignment.getValue() == null) { + return 0; + } + + return assignment.getValue(); + } + + public int getCreatedAt(int furniId, int definitionItemId) { + this.ensurePermanentAssignmentsLoaded(); + + if (furniId <= 0 || definitionItemId <= 0) { + return 0; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definitionItemId); + if (derivedDefinition != null) { + VariableAssignment assignment = this.getRawAssignment(furniId, derivedDefinition.getBaseDefinitionItemId()); + return (assignment != null) ? assignment.getCreatedAt() : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getCreatedAt(this.room, furniId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.get(furniId); + + if (assignments == null) { + return 0; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + return assignment != null ? assignment.getCreatedAt() : 0; + } + + public int getUpdatedAt(int furniId, int definitionItemId) { + this.ensurePermanentAssignmentsLoaded(); + + if (furniId <= 0 || definitionItemId <= 0) { + return 0; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definitionItemId); + if (derivedDefinition != null) { + VariableAssignment assignment = this.getRawAssignment(furniId, derivedDefinition.getBaseDefinitionItemId()); + return (assignment != null) ? assignment.getUpdatedAt() : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getUpdatedAt(this.room, furniId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.get(furniId); + + if (assignments == null) { + return 0; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + return assignment != null ? assignment.getUpdatedAt() : 0; + } + + public boolean hasVariable(int furniId, int definitionItemId) { + this.ensurePermanentAssignmentsLoaded(); + + if (furniId <= 0 || definitionItemId <= 0) { + return false; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definitionItemId); + if (derivedDefinition != null) { + return this.getRawAssignment(furniId, derivedDefinition.getBaseDefinitionItemId()) != null; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).hasVariable(this.room, furniId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.get(furniId); + + return assignments != null && assignments.containsKey(definitionItemId); + } + + public boolean removeVariable(int furniId, int definitionItemId) { + this.ensurePermanentAssignmentsLoaded(); + + if (furniId <= 0 || definitionItemId <= 0) { + return false; + } + + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return false; + } + + boolean hadBefore = this.hasVariable(furniId, definitionItemId); + Integer previousValue = (definitionInfo.hasValue() && hadBefore) ? this.getCurrentValue(furniId, definitionItemId) : null; + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).removeValue(this.room, furniId); + + if (changed) { + boolean hasAfter = this.hasVariable(furniId, definitionItemId); + Integer currentValue = (definitionInfo.hasValue() && hasAfter) ? this.getCurrentValue(furniId, definitionItemId) : null; + this.emitVariableChangedEvents(furniId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.get(furniId); + + if (assignments == null) { + return false; + } + + if (assignments.remove(definitionItemId) == null) { + return false; + } + + if (assignments.isEmpty()) { + this.activeAssignmentsByFurniId.remove(furniId, assignments); + } + + this.deletePersistentAssignment(furniId, definitionItemId); + this.emitVariableChangedEvents(furniId, extra, definitionInfo, hadBefore, previousValue, false, null); + this.broadcastSnapshot(); + + return true; + } + + public void removeAssignmentsForFurni(int furniId) { + this.ensurePermanentAssignmentsLoaded(); + + if (furniId <= 0) { + return; + } + + boolean changed = (this.activeAssignmentsByFurniId.remove(furniId) != null); + this.deletePersistentAssignmentsForFurni(furniId); + + if (changed) { + this.broadcastSnapshot(); + } + } + + public void clearTransientAssignments() { + this.ensurePermanentAssignmentsLoaded(); + + boolean changed = false; + + for (Map.Entry> entry : this.activeAssignmentsByFurniId.entrySet()) { + ConcurrentHashMap assignments = entry.getValue(); + + for (Integer definitionItemId : new ArrayList<>(assignments.keySet())) { + WiredExtraFurniVariable definition = this.getDefinition(definitionItemId); + + if (definition != null && definition.isPermanentAvailability()) { + continue; + } + + if (assignments.remove(definitionItemId) != null) { + changed = true; + } + } + + if (assignments.isEmpty()) { + this.activeAssignmentsByFurniId.remove(entry.getKey(), assignments); + } + } + + if (changed) { + this.broadcastSnapshot(); + } + } + + public void removeDefinition(int definitionItemId) { + this.ensurePermanentAssignmentsLoaded(); + + boolean changed = false; + + for (Map.Entry> entry : this.activeAssignmentsByFurniId.entrySet()) { + ConcurrentHashMap assignments = entry.getValue(); + + if (assignments.remove(definitionItemId) != null) { + changed = true; + } + + if (assignments.isEmpty()) { + this.activeAssignmentsByFurniId.remove(entry.getKey(), assignments); + } + } + + this.deletePersistentAssignmentsForDefinition(definitionItemId); + this.broadcastSnapshot(); + } + + public void handleDefinitionUpdated(WiredExtraFurniVariable definition) { + if (definition == null) { + return; + } + + this.ensurePermanentAssignmentsLoaded(); + + if (!definition.isPermanentAvailability()) { + this.deletePersistentAssignmentsForDefinition(definition.getId()); + } else { + for (Map.Entry> entry : this.activeAssignmentsByFurniId.entrySet()) { + VariableAssignment assignment = entry.getValue().get(definition.getId()); + + if (assignment == null) continue; + + this.upsertPersistentAssignment(entry.getKey(), definition.getId(), assignment); + } + } + + this.broadcastSnapshot(); + } + + public Snapshot createSnapshot() { + this.ensurePermanentAssignmentsLoaded(); + + List definitions = new ArrayList<>(); + Map definitionsById = new LinkedHashMap<>(); + List derivedDefinitionIds = new ArrayList<>(); + List furniEchoes = this.getFurniEchoes(); + + for (WiredVariableDefinitionInfo definition : this.getAllDefinitionInfos()) { + DefinitionEntry entry = new DefinitionEntry(definition.getItemId(), definition.getName(), definition.hasValue(), definition.getAvailability(), definition.isTextConnected(), definition.isReadOnly()); + definitions.add(entry); + definitionsById.put(entry.getItemId(), entry); + + if (WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definition.getItemId()) != null) { + derivedDefinitionIds.add(definition.getItemId()); + } + } + + List furnis = new ArrayList<>(); + THashSet furniIds = new THashSet<>(); + furniIds.addAll(this.activeAssignmentsByFurniId.keySet()); + + for (HabboItem item : this.room.getFloorItems()) { + if (item != null) furniIds.add(item.getId()); + } + + for (HabboItem item : this.room.getWallItems()) { + if (item != null) furniIds.add(item.getId()); + } + + for (Integer furniId : furniIds) { + if (this.room.getHabboItem(furniId) == null) { + continue; + } + + List assignments = new ArrayList<>(); + ConcurrentHashMap localAssignments = this.activeAssignmentsByFurniId.get(furniId); + + if (localAssignments != null) { + for (Map.Entry assignmentEntry : localAssignments.entrySet()) { + if (!definitionsById.containsKey(assignmentEntry.getKey())) { + continue; + } + + assignments.add(new AssignmentEntry( + assignmentEntry.getKey(), + assignmentEntry.getValue().getValue(), + assignmentEntry.getValue().getCreatedAt(), + assignmentEntry.getValue().getUpdatedAt() + )); + } + } + + for (WiredExtraVariableEcho echo : furniEchoes) { + if (!definitionsById.containsKey(echo.getId()) || !echo.hasVariable(this.room, furniId)) { + continue; + } + + assignments.add(new AssignmentEntry( + echo.getId(), + echo.getCurrentValue(this.room, furniId), + echo.getCreatedAt(this.room, furniId), + echo.getUpdatedAt(this.room, furniId) + )); + } + + for (Integer derivedDefinitionId : derivedDefinitionIds) { + if (!this.hasVariable(furniId, derivedDefinitionId)) { + continue; + } + + assignments.add(new AssignmentEntry( + derivedDefinitionId, + this.getCurrentValue(furniId, derivedDefinitionId), + this.getCreatedAt(furniId, derivedDefinitionId), + this.getUpdatedAt(furniId, derivedDefinitionId) + )); + } + + assignments.sort(Comparator.comparingInt(AssignmentEntry::getVariableItemId)); + + if (!assignments.isEmpty()) { + furnis.add(new FurniAssignmentsEntry(furniId, assignments)); + } + } + + furnis.sort(Comparator.comparingInt(FurniAssignmentsEntry::getFurniId)); + + return new Snapshot(this.room.getId(), definitions, furnis); + } + + public void sendSnapshot(Habbo habbo) { + if (habbo == null || habbo.getClient() == null || !this.room.canInspectWired(habbo)) { + return; + } + + habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(this.room.getUserVariableManager().createSnapshot(), this.createSnapshot(), this.room.getRoomVariableManager().createSnapshot())); + } + + public void broadcastSnapshot() { + RoomUserVariableManager.Snapshot userSnapshot = this.room.getUserVariableManager().createSnapshot(); + Snapshot furniSnapshot = this.createSnapshot(); + RoomVariableManager.Snapshot roomSnapshot = this.room.getRoomVariableManager().createSnapshot(); + + for (Habbo habbo : this.room.getCurrentHabbos().values()) { + if (habbo == null || habbo.getClient() == null || !this.room.canInspectWired(habbo)) { + continue; + } + + habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(userSnapshot, furniSnapshot, roomSnapshot)); + } + } + + public Collection getDefinitions() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + THashSet extras = this.room.getRoomSpecialTypes().getExtras(); + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraFurniVariable) { + WiredExtraFurniVariable definition = (WiredExtraFurniVariable) extra; + + if (!hasVisibleDefinitionName(definition.getVariableName())) { + continue; + } + + result.add(definition); + } + } + + result.sort(Comparator.comparing(WiredExtraFurniVariable::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraFurniVariable::getId)); + return result; + } + + public Collection getAllDefinitionInfos() { + List result = new ArrayList<>(); + List baseDefinitions = new ArrayList<>(); + + for (WiredExtraFurniVariable definition : this.getDefinitions()) { + baseDefinitions.add(new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(this.room, definition), + false + )); + } + + for (WiredExtraVariableEcho echo : this.getFurniEchoes()) { + baseDefinitions.add(echo.createDefinitionInfo(this.room)); + } + + result.addAll(baseDefinitions); + + for (WiredVariableDefinitionInfo definition : baseDefinitions) { + result.addAll(WiredVariableLevelSystemSupport.getDerivedDefinitions(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, this.getDefinitionExtra(definition.getItemId()), definition)); + } + + result.sort(Comparator.comparing(WiredVariableDefinitionInfo::getName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredVariableDefinitionInfo::getItemId)); + return result; + } + + public boolean hasDefinition(int definitionItemId) { + return this.getDefinitionInfo(definitionItemId) != null; + } + + public WiredVariableDefinitionInfo getDefinitionInfo(int definitionItemId) { + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + + if (extra instanceof WiredExtraFurniVariable) { + WiredExtraFurniVariable definition = (WiredExtraFurniVariable) extra; + + if (!hasVisibleDefinitionName(definition.getVariableName())) { + return null; + } + + return new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(this.room, definition), + false + ); + } + + if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isFurniEcho()) { + WiredVariableDefinitionInfo info = ((WiredExtraVariableEcho) extra).createDefinitionInfo(this.room); + return (info != null && hasVisibleDefinitionName(info.getName())) ? info : null; + } + + return WiredVariableLevelSystemSupport.getDerivedDefinitionInfo(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definitionItemId); + } + + private WiredExtraFurniVariable getDefinition(int definitionItemId) { + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + + if (!(extra instanceof WiredExtraFurniVariable)) { + return null; + } + + return (WiredExtraFurniVariable) extra; + } + + private InteractionWiredExtra getDefinitionExtra(int definitionItemId) { + if (this.room.getRoomSpecialTypes() == null) { + return null; + } + + return this.room.getRoomSpecialTypes().getExtra(definitionItemId); + } + + private List getFurniEchoes() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { + if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isFurniEcho()) { + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) extra; + + if (!hasVisibleDefinitionName(echo.getVariableName())) { + continue; + } + + result.add(echo); + } + } + + result.sort(Comparator.comparing(WiredExtraVariableEcho::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraVariableEcho::getId)); + return result; + } + + private static boolean hasVisibleDefinitionName(String name) { + return name != null && !name.trim().isEmpty(); + } + + private VariableAssignment getRawAssignment(int furniId, int definitionItemId) { + if (furniId <= 0 || definitionItemId <= 0) { + return null; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) extra; + if (!echo.hasVariable(this.room, furniId)) { + return null; + } + + return new VariableAssignment(echo.getCurrentValue(this.room, furniId), echo.getCreatedAt(this.room, furniId), echo.getUpdatedAt(this.room, furniId)); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByFurniId.get(furniId); + return (assignments != null) ? assignments.get(definitionItemId) : null; + } + + private Integer getRawValue(int furniId, int definitionItemId) { + VariableAssignment assignment = this.getRawAssignment(furniId, definitionItemId); + return (assignment != null) ? assignment.getValue() : null; + } + + private void emitVariableChangedEvents(int furniId, InteractionWiredExtra definitionExtra, WiredVariableDefinitionInfo definitionInfo, boolean existedBefore, Integer previousValue, boolean existsAfter, Integer currentValue) { + if (definitionInfo == null) { + return; + } + + this.emitVariableChangedEvent(furniId, definitionInfo.getItemId(), definitionInfo.hasValue(), existedBefore, previousValue, existsAfter, currentValue); + + for (WiredVariableDefinitionInfo derivedDefinition : WiredVariableLevelSystemSupport.getDerivedDefinitions(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, definitionExtra, definitionInfo)) { + WiredVariableLevelSystemSupport.DerivedDefinition resolvedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_FURNI, derivedDefinition.getItemId()); + + if (resolvedDefinition == null) { + continue; + } + + Integer derivedPreviousValue = existedBefore + ? WiredVariableLevelSystemSupport.getDerivedValue(resolvedDefinition.getLevelSystem(), resolvedDefinition.getSubvariableType(), previousValue) + : null; + Integer derivedCurrentValue = existsAfter + ? WiredVariableLevelSystemSupport.getDerivedValue(resolvedDefinition.getLevelSystem(), resolvedDefinition.getSubvariableType(), currentValue) + : null; + + this.emitVariableChangedEvent(furniId, derivedDefinition.getItemId(), true, existedBefore, derivedPreviousValue, existsAfter, derivedCurrentValue); + } + } + + private void emitVariableChangedEvent(int furniId, int definitionItemId, boolean hasValue, boolean existedBefore, Integer previousValue, boolean existsAfter, Integer currentValue) { + boolean created = !existedBefore && existsAfter; + boolean deleted = existedBefore && !existsAfter; + WiredEvent.VariableChangeKind changeKind = resolveVariableChangeKind(hasValue, existedBefore, previousValue, existsAfter, currentValue); + + if (!created && !deleted && changeKind == WiredEvent.VariableChangeKind.NONE) { + return; + } + + WiredManager.triggerFurniVariableChanged(this.room, furniId, definitionItemId, created, deleted, changeKind); + } + + private static WiredEvent.VariableChangeKind resolveVariableChangeKind(boolean hasValue, boolean existedBefore, Integer previousValue, boolean existsAfter, Integer currentValue) { + if (!hasValue || !existedBefore || !existsAfter) { + return WiredEvent.VariableChangeKind.NONE; + } + + if (Objects.equals(previousValue, currentValue)) { + return WiredEvent.VariableChangeKind.UNCHANGED; + } + + int previousNumericValue = (previousValue != null) ? previousValue : 0; + int currentNumericValue = (currentValue != null) ? currentValue : 0; + + return (currentNumericValue > previousNumericValue) + ? WiredEvent.VariableChangeKind.INCREASED + : WiredEvent.VariableChangeKind.DECREASED; + } + + private void upsertPersistentAssignment(int furniId, int definitionItemId, VariableAssignment assignment) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO room_furni_wired_variables (room_id, furni_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, furniId); + statement.setInt(3, definitionItemId); + + if (assignment == null || assignment.getValue() == null) { + statement.setNull(4, java.sql.Types.INTEGER); + } else { + statement.setInt(4, assignment.getValue()); + } + + int now = Emulator.getIntUnixTimestamp(); + statement.setInt(5, (assignment != null) ? assignment.getCreatedAt() : now); + statement.setInt(6, (assignment != null) ? assignment.getUpdatedAt() : now); + + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to store permanent wired furni variable for room {}, furni {}, item {}", this.room.getId(), furniId, definitionItemId, e); + } + } + + private void deletePersistentAssignment(int furniId, int definitionItemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_furni_wired_variables WHERE room_id = ? AND furni_id = ? AND variable_item_id = ?")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, furniId); + statement.setInt(3, definitionItemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete permanent wired furni variable for room {}, furni {}, item {}", this.room.getId(), furniId, definitionItemId, e); + } + } + + private void deletePersistentAssignmentsForDefinition(int definitionItemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_furni_wired_variables WHERE room_id = ? AND variable_item_id = ?")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, definitionItemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete permanent wired furni variables for room {} and item {}", this.room.getId(), definitionItemId, e); + } + } + + private void deletePersistentAssignmentsForFurni(int furniId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_furni_wired_variables WHERE room_id = ? AND furni_id = ?")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, furniId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete permanent wired furni variables for room {} and furni {}", this.room.getId(), furniId, e); + } + } + + public static class Snapshot { + private final int roomId; + private final List definitions; + private final List furnis; + + public Snapshot(int roomId, List definitions, List furnis) { + this.roomId = roomId; + this.definitions = definitions; + this.furnis = furnis; + } + + public int getRoomId() { + return this.roomId; + } + + public List getDefinitions() { + return this.definitions; + } + + public List getFurnis() { + return this.furnis; + } + } + + public static class DefinitionEntry { + private final int itemId; + private final String name; + private final boolean hasValue; + private final int availability; + private final boolean textConnected; + private final boolean readOnly; + + public DefinitionEntry(int itemId, String name, boolean hasValue, int availability, boolean textConnected, boolean readOnly) { + this.itemId = itemId; + this.name = name; + this.hasValue = hasValue; + this.availability = availability; + this.textConnected = textConnected; + this.readOnly = readOnly; + } + + public int getItemId() { + return this.itemId; + } + + public String getName() { + return this.name; + } + + public boolean hasValue() { + return this.hasValue; + } + + public int getAvailability() { + return this.availability; + } + + public boolean isTextConnected() { + return this.textConnected; + } + + public boolean isReadOnly() { + return this.readOnly; + } + } + + public static class FurniAssignmentsEntry { + private final int furniId; + private final List assignments; + + public FurniAssignmentsEntry(int furniId, List assignments) { + this.furniId = furniId; + this.assignments = assignments; + } + + public int getFurniId() { + return this.furniId; + } + + public List getAssignments() { + return this.assignments; + } + } + + public static class AssignmentEntry { + private final int variableItemId; + private final Integer value; + private final int createdAt; + private final int updatedAt; + + public AssignmentEntry(int variableItemId, Integer value, int createdAt, int updatedAt) { + this.variableItemId = variableItemId; + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public int getVariableItemId() { + return this.variableItemId; + } + + public boolean hasValue() { + return this.value != null; + } + + public Integer getValue() { + return this.value; + } + + public int getCreatedAt() { + return this.createdAt; + } + + public int getUpdatedAt() { + return this.updatedAt; + } + } + + private static class VariableAssignment { + private Integer value; + private final int createdAt; + private int updatedAt; + + public VariableAssignment(Integer value, int createdAt, int updatedAt) { + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Integer getValue() { + return this.value; + } + + public void setValue(Integer value, int updatedAt) { + this.value = value; + this.updatedAt = updatedAt; + } + + public int getCreatedAt() { + return this.createdAt; + } + + public int getUpdatedAt() { + return this.updatedAt; + } + } + + private static int normalizeTimestamp(int value, int fallback) { + if (value > 0) return value; + if (fallback > 0) return fallback; + return Emulator.getIntUnixTimestamp(); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomHanditemBlockSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomHanditemBlockSupport.java new file mode 100644 index 00000000..b35025e5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomHanditemBlockSupport.java @@ -0,0 +1,65 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.habbohotel.items.interactions.InteractionHanditemBlockControl; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.messages.outgoing.rooms.items.HanditemBlockStateComposer; + +public final class RoomHanditemBlockSupport { + private static final String CONTROLLER_INTERACTION = "wf_conf_handitem_block"; + + private RoomHanditemBlockSupport() { + } + + public static boolean isHanditemBlocked(Room room) { + if (room == null) { + return false; + } + + for (HabboItem item : room.getFloorItems()) { + if (isActiveController(item)) { + return true; + } + } + + return false; + } + + public static boolean isActiveController(HabboItem item) { + return isControllerItem(item) && "1".equals(item.getExtradata()); + } + + public static boolean isControllerItem(HabboItem item) { + if (item == null || item.getBaseItem() == null) { + return false; + } + + if (item instanceof InteractionHanditemBlockControl) { + return true; + } + + if (item.getBaseItem().getInteractionType() == null) { + return false; + } + + String interactionName = item.getBaseItem().getInteractionType().getName(); + + return interactionName != null && interactionName.equalsIgnoreCase(CONTROLLER_INTERACTION); + } + + public static void sendState(Room room) { + if (room == null) { + return; + } + + room.sendComposer(new HanditemBlockStateComposer(room).compose()); + } + + public static void sendState(Room room, GameClient client) { + if (room == null || client == null) { + return; + } + + client.sendResponse(new HanditemBlockStateComposer(room).compose()); + } +} 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 3f1d8ccf..a40bdbf6 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 @@ -16,6 +16,14 @@ 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.triggers.WiredTriggerReceiveSignal; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.users.Habbo; @@ -23,6 +31,7 @@ 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.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.rooms.items.*; @@ -618,19 +627,28 @@ public class RoomItemManager { } } + if (BuildersClubRoomSupport.isTrackedItem(item.getId()) && item.getUserId() != BuildersClubRoomSupport.VIRTUAL_OWNER_ID) { + item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID); + item.needsUpdate(true); + } + synchronized (this.furniOwnerCount) { this.furniOwnerCount.put(item.getUserId(), this.furniOwnerCount.get(item.getUserId()) + 1); } synchronized (this.furniOwnerNames) { if (!this.furniOwnerNames.containsKey(item.getUserId())) { - HabboInfo habbo = HabboManager.getOfflineHabboInfo(item.getUserId()); - - if (habbo != null) { - this.furniOwnerNames.put(item.getUserId(), habbo.getUsername()); + if (item.getUserId() == BuildersClubRoomSupport.VIRTUAL_OWNER_ID && BuildersClubRoomSupport.isTrackedItem(item.getId())) { + this.furniOwnerNames.put(item.getUserId(), BuildersClubRoomSupport.DISPLAY_OWNER_NAME); } else { - LOGGER.error("Failed to find username for item (ID: {}, UserID: {})", - item.getId(), item.getUserId()); + HabboInfo habbo = HabboManager.getOfflineHabboInfo(item.getUserId()); + + if (habbo != null) { + this.furniOwnerNames.put(item.getUserId(), habbo.getUsername()); + } else { + LOGGER.error("Failed to find username for item (ID: {}, UserID: {})", + item.getId(), item.getUserId()); + } } } } @@ -740,6 +758,9 @@ public class RoomItemManager { return; } + boolean trackedBuildersClubItem = BuildersClubRoomSupport.isTrackedItem(item.getId()); + int trackedUserId = trackedBuildersClubItem ? BuildersClubRoomSupport.getTrackedUserId(item.getId()) : item.getUserId(); + HabboItem i; synchronized (this.roomItems) { i = this.roomItems.remove(item.getId()); @@ -762,6 +783,16 @@ public class RoomItemManager { // Unregister from special types this.unregisterItemFromSpecialTypes(item); } + + if (trackedBuildersClubItem) { + BuildersClubRoomSupport.deleteTrackedItem(item.getId()); + + if (BuildersClubRoomSupport.syncRoom(this.room) == BuildersClubRoomSupport.SyncResult.UNLOCKED) { + BuildersClubRoomSupport.sendRoomUnlockedBubble(this.room.getOwnerId()); + } + + BuildersClubRoomSupport.sendPlacementStatusForPool(this.room, trackedUserId); + } } /** @@ -772,6 +803,13 @@ public class RoomItemManager { if (specialTypes == null) { return; } + + boolean cleanedSignalAntennaReferences = false; + if (this.isAntennaItem(item)) { + cleanedSignalAntennaReferences = specialTypes.unlinkSignalAntennaReferences(item.getId()); + } + + this.room.getFurniVariableManager().removeAssignmentsForFurni(item.getId()); boolean isWiredItem = false; @@ -784,21 +822,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) { - specialTypes.removeExtra((InteractionWiredExtra) item); - isWiredItem = true; - } else if (item instanceof InteractionRoller) { + 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) { specialTypes.removeRoller((InteractionRoller) item); } else if (item instanceof InteractionGameScoreboard) { specialTypes.removeScoreboard((InteractionGameScoreboard) item); @@ -840,11 +910,20 @@ public class RoomItemManager { } // Invalidate wired cache when wired items are removed - if (isWiredItem) { + if (isWiredItem || cleanedSignalAntennaReferences) { WiredManager.invalidateRoom(this.room); } } + private boolean isAntennaItem(HabboItem item) { + if (item == null || item.getBaseItem() == null || item.getBaseItem().getInteractionType() == null) { + return false; + } + + String interactionType = item.getBaseItem().getInteractionType().getName(); + return interactionType != null && interactionType.equalsIgnoreCase("antenna"); + } + // ==================== ITEM UPDATES ==================== /** @@ -949,6 +1028,8 @@ public class RoomItemManager { return; } + boolean trackedBuildersClubItem = BuildersClubRoomSupport.isTrackedItem(item.getId()); + if (Emulator.getPluginManager().isRegistered(FurniturePickedUpEvent.class, true)) { FurniturePickedUpEvent event = Emulator.getPluginManager() .fireEvent(new FurniturePickedUpEvent(item, picker)); @@ -966,6 +1047,18 @@ public class RoomItemManager { if (item.getBaseItem().getType() == FurnitureType.FLOOR) { this.room.sendComposer(new RemoveFloorItemComposer(item).compose()); + if (RoomConfInvisSupport.isControllerItem(item) || RoomConfInvisSupport.isTarget(item)) { + RoomConfInvisSupport.sendState(this.room); + } + + if (RoomAreaHideSupport.isControllerItem(item)) { + RoomAreaHideSupport.sendState(this.room, item); + } + + if (RoomHanditemBlockSupport.isControllerItem(item)) { + RoomHanditemBlockSupport.sendState(this.room); + } + THashSet updatedTiles = this.room.getLayout().getTilesAt( this.room.getLayout().getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(), @@ -981,6 +1074,11 @@ public class RoomItemManager { this.room.sendComposer(new RemoveWallItemComposer(item).compose()); } + if (trackedBuildersClubItem) { + Emulator.getGameEnvironment().getItemManager().deleteItem(item); + return; + } + Emulator.getThreading().run(item); } @@ -989,6 +1087,7 @@ public class RoomItemManager { */ public void ejectUserFurni(int userId) { THashSet items = new THashSet<>(); + THashSet inventoryItems = new THashSet<>(); TIntObjectIterator iterator = this.roomItems.iterator(); @@ -1001,15 +1100,20 @@ public class RoomItemManager { if (iterator.value().getUserId() == userId) { items.add(iterator.value()); + + if (!BuildersClubRoomSupport.isTrackedItem(iterator.value().getId())) { + inventoryItems.add(iterator.value()); + } + iterator.value().setRoomId(0); } } Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); - if (habbo != null) { - habbo.getInventory().getItemsComponent().addItems(items); - habbo.getClient().sendResponse(new AddHabboItemComposer(items)); + if (habbo != null && !inventoryItems.isEmpty()) { + habbo.getInventory().getItemsComponent().addItems(inventoryItems); + habbo.getClient().sendResponse(new AddHabboItemComposer(inventoryItems)); } for (HabboItem i : items) { @@ -1061,15 +1165,23 @@ public class RoomItemManager { } for (Map.Entry> entrySet : userItemsMap.entrySet()) { + THashSet inventoryItems = new THashSet<>(); + + for (HabboItem item : entrySet.getValue()) { + if (!BuildersClubRoomSupport.isTrackedItem(item.getId())) { + inventoryItems.add(item); + } + } + for (HabboItem i : entrySet.getValue()) { this.pickUpItem(i, null); } Habbo user = Emulator.getGameEnvironment().getHabboManager().getHabbo(entrySet.getKey()); - if (user != null) { - user.getInventory().getItemsComponent().addItems(entrySet.getValue()); - user.getClient().sendResponse(new AddHabboItemComposer(entrySet.getValue())); + if (user != null && !inventoryItems.isEmpty()) { + user.getInventory().getItemsComponent().addItems(inventoryItems); + user.getClient().sendResponse(new AddHabboItemComposer(inventoryItems)); } } } @@ -1213,7 +1325,7 @@ public class RoomItemManager { rotation %= 8; if (this.room.hasRights(habbo) || this.room.getGuildRightLevel(habbo) .isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || habbo.hasPermission( - Permission.ACC_MOVEROTATE)) { + Permission.ACC_MOVEROTATE) || BuildersClubRoomSupport.canPlaceInRoom(habbo, this.room)) { return FurnitureMovementError.NONE; } @@ -1253,13 +1365,75 @@ public class RoomItemManager { /** * Checks if furniture fits at a location with unit check option. */ + private boolean isStackPlacementBypassItem(HabboItem item) { + return item instanceof InteractionStackHelper + || item instanceof InteractionTileWalkMagic + || item instanceof InteractionStackWalkHelper; + } + + private boolean shouldPinStackHelperToFloor(HabboItem item) { + return item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic; + } + + private boolean isStackHeightHelper(HabboItem item) { + return item instanceof InteractionStackHelper + || item instanceof InteractionTileWalkMagic + || item instanceof InteractionStackWalkHelper; + } + + private HabboItem findStackHeightHelperAt(RoomTile tile, HabboItem exclude) { + if (tile == null) { + return null; + } + + for (HabboItem helper : this.getItemsAt(tile)) { + if (helper != exclude && this.isStackHeightHelper(helper)) { + return helper; + } + } + + return null; + } + + private double getMinimumTileHeight(THashSet occupiedTiles) { + double minimumHeight = 0.0D; + + for (RoomTile occupiedTile : occupiedTiles) { + minimumHeight = Math.max(minimumHeight, this.room.getLayout().getHeightAtSquare(occupiedTile.x, occupiedTile.y)); + } + + return minimumHeight; + } + + private double getConfiguredStackWalkHelperHeight(HabboItem item, THashSet occupiedTiles) { + double height = 0.0D; + + try { + if (item.getExtradata() != null && !item.getExtradata().isEmpty()) { + height = Double.parseDouble(item.getExtradata()) / 100.0D; + } + } catch (NumberFormatException ignored) { + } + + return Math.max(height, this.getMinimumTileHeight(occupiedTiles)); + } + + private double resolveStackWalkHelperHeight(HabboItem item, RoomTile tile, THashSet occupiedTiles) { + HabboItem helper = this.findStackHeightHelperAt(tile, item); + if (helper != null) { + return Math.max(helper.getZ(), this.getMinimumTileHeight(occupiedTiles)); + } + + return this.getConfiguredStackWalkHelperHeight(item, occupiedTiles); + } + public FurnitureMovementError furnitureFitsAt(RoomTile tile, HabboItem item, int rotation, boolean checkForUnits) { RoomLayout layout = this.room.getLayout(); if (!layout.fitsOnMap(tile, item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation)) { return FurnitureMovementError.INVALID_MOVE; } - if (item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic) { + if (this.isStackPlacementBypassItem(item)) { return FurnitureMovementError.NONE; } @@ -1301,6 +1475,56 @@ public class RoomItemManager { return FurnitureMovementError.NONE; } + public FurnitureMovementError furnitureFitsAtWithPhysics(RoomTile tile, HabboItem item, int rotation, boolean checkForUnits, WiredMovementPhysics physics) { + if (physics == null || !physics.isActive()) { + return furnitureFitsAt(tile, item, rotation, checkForUnits); + } + + RoomLayout layout = this.room.getLayout(); + if (!layout.fitsOnMap(tile, item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation)) { + return FurnitureMovementError.INVALID_MOVE; + } + + if (this.isStackPlacementBypassItem(item)) { + return FurnitureMovementError.NONE; + } + + THashSet occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(), + item.getBaseItem().getLength(), rotation); + for (RoomTile t : occupiedTiles) { + if (t.state == RoomTileState.INVALID) { + return FurnitureMovementError.INVALID_MOVE; + } + + if (shouldCheckUnits(item, checkForUnits)) { + FurnitureMovementError unitCollision = this.getPhysicsUnitCollision(t, physics); + if (unitCollision != FurnitureMovementError.NONE) { + return unitCollision; + } + } + } + + if (this.hasBlockingPhysicsFurni(occupiedTiles, item, physics)) { + return FurnitureMovementError.CANT_STACK; + } + + java.util.List>> tileFurniList = new java.util.ArrayList<>(); + for (RoomTile t : occupiedTiles) { + tileFurniList.add(Pair.create(t, this.getPhysicsItemsAt(t, item, physics))); + + HabboItem topItem = this.getTopPhysicsItemAt(t.x, t.y, item, physics); + if (topItem != null && !topItem.getBaseItem().allowStack() && !t.getAllowStack()) { + return FurnitureMovementError.CANT_STACK; + } + } + + if (!item.canStackAt(this.room, tileFurniList)) { + return FurnitureMovementError.CANT_STACK; + } + + return FurnitureMovementError.NONE; + } + /** * Places a floor furniture item at a position. */ @@ -1359,7 +1583,7 @@ public class RoomItemManager { item.setY(tile.y); item.setRotation(rotation); if (!this.furniOwnerNames.containsKey(item.getUserId()) && owner != null) { - this.furniOwnerNames.put(item.getUserId(), owner.getHabboInfo().getUsername()); + this.furniOwnerNames.put(item.getUserId(), this.resolveOwnerName(item, owner)); } item.needsUpdate(true); @@ -1370,6 +1594,18 @@ public class RoomItemManager { this.room.sendComposer( new AddFloorItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose()); + if (RoomConfInvisSupport.isControllerItem(item) || RoomConfInvisSupport.isTarget(item)) { + RoomConfInvisSupport.sendState(this.room); + } + + if (RoomAreaHideSupport.isControllerItem(item)) { + RoomAreaHideSupport.sendState(this.room, item); + } + + if (RoomHanditemBlockSupport.isControllerItem(item)) { + RoomHanditemBlockSupport.sendState(this.room); + } + for (RoomTile t : occupiedTiles) { this.room.updateHabbosAt(t.x, t.y); this.room.updateBotsAt(t.x, t.y); @@ -1384,7 +1620,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))) { + .isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS) || BuildersClubRoomSupport.canPlaceInRoom(owner, this.room))) { return FurnitureMovementError.NO_RIGHTS; } @@ -1399,7 +1635,7 @@ public class RoomItemManager { item.setWallPosition(wallPosition); if (!this.furniOwnerNames.containsKey(item.getUserId()) && owner != null) { - this.furniOwnerNames.put(item.getUserId(), owner.getHabboInfo().getUsername()); + this.furniOwnerNames.put(item.getUserId(), this.resolveOwnerName(item, owner)); } this.room.sendComposer( new AddWallItemComposer(item, this.getFurniOwnerName(item.getUserId())).compose()); @@ -1411,6 +1647,14 @@ public class RoomItemManager { return FurnitureMovementError.NONE; } + private String resolveOwnerName(HabboItem item, Habbo owner) { + if (item != null && item.getUserId() == BuildersClubRoomSupport.VIRTUAL_OWNER_ID) { + return BuildersClubRoomSupport.DISPLAY_OWNER_NAME; + } + + return (owner != null) ? owner.getHabboInfo().getUsername() : ""; + } + /** * Moves furniture to a new position with an explicit Z height. */ @@ -1437,9 +1681,7 @@ public class RoomItemManager { rotation %= 8; - boolean magicTile = - item instanceof InteractionStackHelper || - item instanceof InteractionTileWalkMagic; + boolean magicTile = this.isStackPlacementBypassItem(item); THashSet occupiedTiles = layout.getTilesAt( tile, @@ -1502,9 +1744,11 @@ public class RoomItemManager { item.setY(tile.y); item.setZ(z); - if (magicTile) { + if (this.shouldPinStackHelperToFloor(item)) { item.setZ(tile.z); item.setExtradata("" + (item.getZ() * 100)); + } else if (item instanceof InteractionStackWalkHelper) { + item.setZ(this.resolveStackWalkHelperHeight(item, tile, occupiedTiles)); } if (item.getZ() > Room.MAXIMUM_FURNI_HEIGHT) { @@ -1569,6 +1813,158 @@ public class RoomItemManager { return FurnitureMovementError.NONE; } + public FurnitureMovementError moveFurniToWithPhysics(HabboItem item, RoomTile tile, int rotation, double z, Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) { + if (physics == null || !physics.isActive()) { + return moveFurniTo(item, tile, rotation, z, actor, sendUpdates, checkForUnits); + } + + if (item == null || tile == null) { + return FurnitureMovementError.INVALID_MOVE; + } + + RoomLayout layout = this.room.getLayout(); + RoomTile oldLocation = layout.getTile(item.getX(), item.getY()); + + boolean pluginHelper = false; + + if (Emulator.getPluginManager().isRegistered(FurnitureMovedEvent.class, true)) { + FurnitureMovedEvent event = Emulator.getPluginManager() + .fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile)); + + if (event.isCancelled()) { + return FurnitureMovementError.CANCEL_PLUGIN_MOVE; + } + + pluginHelper = event.hasPluginHelper(); + } + + rotation %= 8; + + boolean magicTile = this.isStackPlacementBypassItem(item); + + THashSet occupiedTiles = layout.getTilesAt( + tile, + item.getBaseItem().getWidth(), + item.getBaseItem().getLength(), + rotation + ); + + THashSet oldOccupiedTiles = layout.getTilesAt( + layout.getTile(item.getX(), item.getY()), + item.getBaseItem().getWidth(), + item.getBaseItem().getLength(), + item.getRotation() + ); + + if (!pluginHelper) { + FurnitureMovementError fits = furnitureFitsAtWithPhysics(tile, item, rotation, checkForUnits, physics); + if (fits != FurnitureMovementError.NONE) { + return fits; + } + } + + int oldRotation = item.getRotation(); + + if (oldRotation != rotation) { + item.setRotation(rotation); + + if (Emulator.getPluginManager().isRegistered(FurnitureRotatedEvent.class, true)) { + Event rotatedEvent = new FurnitureRotatedEvent(item, actor, oldRotation); + Emulator.getPluginManager().fireEvent(rotatedEvent); + + if (rotatedEvent.isCancelled()) { + item.setRotation(oldRotation); + return FurnitureMovementError.CANCEL_PLUGIN_ROTATE; + } + } + } + + if (z > Room.MAXIMUM_FURNI_HEIGHT) { + return FurnitureMovementError.CANT_STACK; + } + + if (z < layout.getHeightAtSquare(tile.x, tile.y)) { + return FurnitureMovementError.CANT_STACK; + } + + if (Emulator.getPluginManager().isRegistered(FurnitureBuildheightEvent.class, true)) { + FurnitureBuildheightEvent event = Emulator.getPluginManager() + .fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, z)); + + if (event.hasChangedHeight()) { + z = layout.getHeightAtSquare(tile.x, tile.y) + event.getUpdatedHeight(); + } + } + + item.setX(tile.x); + item.setY(tile.y); + item.setZ(z); + + if (this.shouldPinStackHelperToFloor(item)) { + item.setZ(tile.z); + item.setExtradata("" + (item.getZ() * 100)); + } else if (item instanceof InteractionStackWalkHelper) { + item.setZ(this.resolveStackWalkHelperHeight(item, tile, occupiedTiles)); + } + + if (item.getZ() > Room.MAXIMUM_FURNI_HEIGHT) { + item.setZ(Room.MAXIMUM_FURNI_HEIGHT); + } + + if (oldLocation != null) { + if (item instanceof InteractionWiredTrigger) { + this.room.getRoomSpecialTypes().updateTriggerLocation((InteractionWiredTrigger) item, oldLocation.x, oldLocation.y); + WiredManager.invalidateRoom(this.room); + } else if (item instanceof InteractionWiredEffect) { + this.room.getRoomSpecialTypes().updateEffectLocation((InteractionWiredEffect) item, oldLocation.x, oldLocation.y); + WiredManager.invalidateRoom(this.room); + } else if (item instanceof InteractionWiredCondition) { + this.room.getRoomSpecialTypes().updateConditionLocation((InteractionWiredCondition) item, oldLocation.x, oldLocation.y); + WiredManager.invalidateRoom(this.room); + } else if (item instanceof InteractionWiredExtra) { + this.room.getRoomSpecialTypes().updateExtraLocation((InteractionWiredExtra) item, oldLocation.x, oldLocation.y); + WiredManager.invalidateRoom(this.room); + } + } + + item.onMove(this.room, oldLocation, tile); + item.needsUpdate(true); + Emulator.getThreading().run(item); + + if (sendUpdates) { + this.room.sendComposer(new FloorItemUpdateComposer(item).compose()); + } + + occupiedTiles.removeAll(oldOccupiedTiles); + occupiedTiles.addAll(oldOccupiedTiles); + this.room.updateTiles(occupiedTiles); + + for (RoomTile t : occupiedTiles) { + this.room.updateHabbosAt(t.x, t.y, this.room.getHabbosAt(t.x, t.y)); + this.room.updateBotsAt(t.x, t.y); + } + + if (Emulator.getConfig().getBoolean("wired.place.under", false)) { + THashSet newOccupiedTiles = layout.getTilesAt( + tile, + item.getBaseItem().getWidth(), + item.getBaseItem().getLength(), + rotation + ); + + for (RoomTile t : newOccupiedTiles) { + for (Habbo h : this.room.getHabbosAt(t.x, t.y)) { + try { + item.onWalkOn(h.getRoomUnit(), this.room, null); + } catch (Exception ignored) { + } + } + } + } + + return FurnitureMovementError.NONE; + } + /** * Moves furniture to a new position. */ @@ -1600,10 +1996,9 @@ public class RoomItemManager { pluginHelper = event.hasPluginHelper(); } - boolean magicTile = item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic; + boolean magicTile = this.isStackPlacementBypassItem(item); - java.util.Optional stackHelper = this.getItemsAt(tile).stream() - .filter(i -> i instanceof InteractionStackHelper).findAny(); + HabboItem stackHelper = this.findStackHeightHelperAt(tile, item); // Check if can be placed at new position THashSet occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(), @@ -1613,7 +2008,7 @@ public class RoomItemManager { HabboItem topItem = this.getTopItemAt(occupiedTiles, null); - if (!stackHelper.isPresent() && !pluginHelper) { + if (stackHelper == null && !pluginHelper) { if (oldLocation != tile) { for (RoomTile t : occupiedTiles) { HabboItem tileTopItem = this.getTopItemAt(t.x, t.y); @@ -1670,7 +2065,7 @@ public class RoomItemManager { } } - if ((!stackHelper.isPresent() && topItem != null && topItem != item && !topItem.getBaseItem() + 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)) { @@ -1682,9 +2077,10 @@ public class RoomItemManager { // Place at new position double height; - if (stackHelper.isPresent()) { - height = stackHelper.get().getExtradata().isEmpty() ? Double.parseDouble("0.0") - : (Double.parseDouble(stackHelper.get().getExtradata()) / 100); + if (stackHelper != null) { + height = stackHelper.getZ(); + } else if (item instanceof InteractionStackWalkHelper) { + height = this.resolveStackWalkHelperHeight(item, tile, occupiedTiles); } else if (item == topItem) { height = item.getZ(); } else if (magicTile) { @@ -1735,7 +2131,7 @@ public class RoomItemManager { item.setX(tile.x); item.setY(tile.y); item.setZ(height); - if (magicTile) { + if (this.shouldPinStackHelperToFloor(item)) { item.setZ(tile.z); item.setExtradata("" + item.getZ() * 100); } @@ -1791,11 +2187,208 @@ public class RoomItemManager { return FurnitureMovementError.NONE; } + public FurnitureMovementError moveFurniToWithPhysics(HabboItem item, RoomTile tile, int rotation, Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) { + if (physics == null || !physics.isActive()) { + return moveFurniTo(item, tile, rotation, actor, sendUpdates, checkForUnits); + } + + RoomLayout layout = this.room.getLayout(); + RoomTile oldLocation = layout.getTile(item.getX(), item.getY()); + + boolean pluginHelper = false; + if (Emulator.getPluginManager().isRegistered(FurnitureMovedEvent.class, true)) { + FurnitureMovedEvent event = Emulator.getPluginManager() + .fireEvent(new FurnitureMovedEvent(item, actor, oldLocation, tile)); + if (event.isCancelled()) { + return FurnitureMovementError.CANCEL_PLUGIN_MOVE; + } + pluginHelper = event.hasPluginHelper(); + } + + boolean magicTile = this.isStackPlacementBypassItem(item); + + HabboItem stackHelper = this.findStackHeightHelperAt(tile, item); + + THashSet occupiedTiles = layout.getTilesAt(tile, item.getBaseItem().getWidth(), + item.getBaseItem().getLength(), rotation); + THashSet newOccupiedTiles = layout.getTilesAt(tile, + item.getBaseItem().getWidth(), item.getBaseItem().getLength(), rotation); + + HabboItem topItem = this.getTopPhysicsItemAt(occupiedTiles, null, physics); + + if (stackHelper == null && !pluginHelper) { + if (oldLocation != tile) { + 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() + || !tileTopItem.getBaseItem().allowStack()) + : this.room.calculateTileState(t, item).equals(RoomTileState.INVALID)))) { + return FurnitureMovementError.CANT_STACK; + } + + if (shouldCheckUnits(item, checkForUnits)) { + FurnitureMovementError unitCollision = this.getPhysicsUnitCollision(t, physics); + if (!magicTile && unitCollision != FurnitureMovementError.NONE) { + return unitCollision; + } + } + } + } + + if (this.hasBlockingPhysicsFurni(occupiedTiles, item, physics)) { + return FurnitureMovementError.CANT_STACK; + } + + java.util.List>> tileFurniList = new java.util.ArrayList<>(); + for (RoomTile t : occupiedTiles) { + tileFurniList.add(Pair.create(t, this.getPhysicsItemsAt(t, item, physics))); + } + + if (!magicTile && !item.canStackAt(this.room, tileFurniList)) { + return FurnitureMovementError.CANT_STACK; + } + } + + THashSet oldOccupiedTiles = layout.getTilesAt( + layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(), + item.getBaseItem().getLength(), item.getRotation()); + + int oldRotation = item.getRotation(); + + if (oldRotation != rotation) { + item.setRotation(rotation); + if (Emulator.getPluginManager().isRegistered(FurnitureRotatedEvent.class, true)) { + Event furnitureRotatedEvent = new FurnitureRotatedEvent(item, actor, oldRotation); + Emulator.getPluginManager().fireEvent(furnitureRotatedEvent); + + if (furnitureRotatedEvent.isCancelled()) { + item.setRotation(oldRotation); + return FurnitureMovementError.CANCEL_PLUGIN_ROTATE; + } + } + + 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)) { + item.setRotation(oldRotation); + return FurnitureMovementError.CANT_STACK; + } + } + + double height; + + if (stackHelper != null) { + height = stackHelper.getZ(); + } else if (item instanceof InteractionStackWalkHelper) { + height = this.resolveStackWalkHelperHeight(item, tile, occupiedTiles); + } else if (item == topItem) { + height = item.getZ(); + } else if (magicTile) { + if (topItem == null) { + height = this.getPhysicsStackHeight(tile.x, tile.y, item, physics); + for (RoomTile til : occupiedTiles) { + double sHeight = this.getPhysicsStackHeight(til.x, til.y, item, physics); + if (sHeight > height) { + height = sHeight; + } + } + } else { + height = topItem.getZ() + topItem.getBaseItem().getHeight(); + } + } else { + height = this.getPhysicsStackHeight(tile.x, tile.y, item, physics); + for (RoomTile til : occupiedTiles) { + double sHeight = this.getPhysicsStackHeight(til.x, til.y, item, physics); + if (sHeight > height) { + height = sHeight; + } + } + } + + boolean cantStack = false; + boolean pluginHeight = false; + + if (height > Room.MAXIMUM_FURNI_HEIGHT) { + cantStack = true; + } + if (height < layout.getHeightAtSquare(tile.x, tile.y)) { + cantStack = true; + } + + if (Emulator.getPluginManager().isRegistered(FurnitureBuildheightEvent.class, true)) { + FurnitureBuildheightEvent event = Emulator.getPluginManager() + .fireEvent(new FurnitureBuildheightEvent(item, actor, 0.00, height)); + if (event.hasChangedHeight()) { + height = layout.getHeightAtSquare(tile.x, tile.y) + event.getUpdatedHeight(); + pluginHeight = true; + } + } + + if (!pluginHeight && cantStack) { + return FurnitureMovementError.CANT_STACK; + } + + item.setX(tile.x); + item.setY(tile.y); + item.setZ(height); + if (this.shouldPinStackHelperToFloor(item)) { + item.setZ(tile.z); + item.setExtradata("" + item.getZ() * 100); + } + if (item.getZ() > Room.MAXIMUM_FURNI_HEIGHT) { + item.setZ(Room.MAXIMUM_FURNI_HEIGHT); + } + + if (item instanceof InteractionWiredTrigger) { + this.room.getRoomSpecialTypes().updateTriggerLocation((InteractionWiredTrigger) item, oldLocation.x, oldLocation.y); + WiredManager.invalidateRoom(this.room); + } else if (item instanceof InteractionWiredEffect) { + this.room.getRoomSpecialTypes().updateEffectLocation((InteractionWiredEffect) item, oldLocation.x, oldLocation.y); + WiredManager.invalidateRoom(this.room); + } else if (item instanceof InteractionWiredCondition) { + this.room.getRoomSpecialTypes().updateConditionLocation((InteractionWiredCondition) item, oldLocation.x, oldLocation.y); + WiredManager.invalidateRoom(this.room); + } else if (item instanceof InteractionWiredExtra) { + this.room.getRoomSpecialTypes().updateExtraLocation((InteractionWiredExtra) item, oldLocation.x, oldLocation.y); + WiredManager.invalidateRoom(this.room); + } + + item.onMove(this.room, oldLocation, tile); + item.needsUpdate(true); + Emulator.getThreading().run(item); + + if (sendUpdates) { + this.room.sendComposer(new FloorItemUpdateComposer(item).compose()); + } + + occupiedTiles.removeAll(oldOccupiedTiles); + occupiedTiles.addAll(oldOccupiedTiles); + this.room.updateTiles(occupiedTiles); + + for (RoomTile t : occupiedTiles) { + this.room.updateHabbosAt(t.x, t.y, this.room.getHabbosAt(t.x, t.y)); + this.room.updateBotsAt(t.x, t.y); + } + if (Emulator.getConfig().getBoolean("wired.place.under", false)) { + for (RoomTile t : newOccupiedTiles) { + for (Habbo h : this.room.getHabbosAt(t.x, t.y)) { + try { + item.onWalkOn(h.getRoomUnit(), this.room, null); + } catch (Exception e) { + } + } + } + } + return FurnitureMovementError.NONE; + } + /** * Slides furniture to a new position. */ public FurnitureMovementError slideFurniTo(HabboItem item, RoomTile tile, int rotation) { - boolean magicTile = item instanceof InteractionStackHelper; + boolean magicTile = this.isStackPlacementBypassItem(item); RoomLayout layout = this.room.getLayout(); @@ -1815,9 +2408,11 @@ public class RoomItemManager { item.setRotation(rotation); // Place at new position - if (magicTile) { + if (this.shouldPinStackHelperToFloor(item)) { item.setZ(tile.z); item.setExtradata("" + item.getZ() * 100); + } else if (item instanceof InteractionStackWalkHelper) { + item.setZ(this.resolveStackWalkHelperHeight(item, tile, occupiedTiles)); } if (item.getZ() > Room.MAXIMUM_FURNI_HEIGHT) { item.setZ(Room.MAXIMUM_FURNI_HEIGHT); @@ -1832,4 +2427,158 @@ public class RoomItemManager { } return FurnitureMovementError.NONE; } + + private boolean shouldCheckUnits(HabboItem item, boolean checkForUnits) { + if (!checkForUnits) { + return false; + } + + if (!Emulator.getConfig().getBoolean("wired.place.under", false)) { + return true; + } + + return !item.isWalkable() + && !item.getBaseItem().allowSit() + && !item.getBaseItem().allowLay(); + } + + private FurnitureMovementError getPhysicsUnitCollision(RoomTile tile, WiredMovementPhysics physics) { + for (RoomUnit roomUnit : this.room.getRoomUnits(tile)) { + if (roomUnit == null) { + continue; + } + + switch (roomUnit.getRoomUnitType()) { + case BOT: + return FurnitureMovementError.TILE_HAS_BOTS; + case PET: + return FurnitureMovementError.TILE_HAS_PETS; + case USER: + if (physics == null || !physics.shouldIgnoreUser(roomUnit)) { + return FurnitureMovementError.TILE_HAS_HABBOS; + } + break; + default: + return FurnitureMovementError.TILE_HAS_HABBOS; + } + } + + return FurnitureMovementError.NONE; + } + + private boolean hasBlockingPhysicsFurni(THashSet occupiedTiles, HabboItem exclude, WiredMovementPhysics physics) { + if (physics == null || !physics.hasBlockingFurni()) { + return false; + } + + for (RoomTile tile : occupiedTiles) { + for (HabboItem item : this.getItemsAt(tile)) { + if (item == null || item == exclude) { + continue; + } + + if (physics.isBlockingFurni(item)) { + return true; + } + } + } + + return false; + } + + private THashSet getPhysicsItemsAt(RoomTile tile, HabboItem exclude, WiredMovementPhysics physics) { + THashSet items = new THashSet<>(); + + for (HabboItem item : this.getItemsAt(tile)) { + if (item == null || item == exclude) { + continue; + } + + if (physics != null && physics.shouldIgnoreFurni(item)) { + continue; + } + + items.add(item); + } + + return items; + } + + private HabboItem getTopPhysicsItemAt(int x, int y, HabboItem exclude, WiredMovementPhysics physics) { + RoomTile tile = this.room.getLayout().getTile((short) x, (short) y); + + if (tile == null) { + return null; + } + + HabboItem highestItem = null; + + for (HabboItem item : this.getPhysicsItemsAt(tile, exclude, physics)) { + if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem) + > item.getZ() + Item.getCurrentHeight(item)) { + continue; + } + + highestItem = item; + } + + return highestItem; + } + + private HabboItem getTopPhysicsItemAt(THashSet tiles, HabboItem exclude, WiredMovementPhysics physics) { + HabboItem highestItem = null; + + for (RoomTile tile : tiles) { + if (tile == null) { + continue; + } + + HabboItem topItem = this.getTopPhysicsItemAt(tile.x, tile.y, exclude, physics); + if (topItem == null) { + continue; + } + + if (highestItem != null && highestItem.getZ() + Item.getCurrentHeight(highestItem) + > topItem.getZ() + Item.getCurrentHeight(topItem)) { + continue; + } + + highestItem = topItem; + } + + return highestItem; + } + + private double getPhysicsStackHeight(short x, short y, HabboItem exclude, WiredMovementPhysics physics) { + RoomLayout layout = this.room.getLayout(); + + if (x < 0 || y < 0 || layout == null) { + return 0.0; + } + + double height = layout.getHeightAtSquare(x, y); + + RoomTile tile = layout.getTile(x, y); + if (tile == null) { + return height; + } + + double helperHeight = Double.NEGATIVE_INFINITY; + for (HabboItem item : this.getPhysicsItemsAt(tile, exclude, physics)) { + if (item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic || item instanceof InteractionStackWalkHelper) { + helperHeight = Math.max(helperHeight, item.getZ()); + } + } + + if (helperHeight != Double.NEGATIVE_INFINITY) { + return helperHeight; + } + + HabboItem topItem = this.getTopPhysicsItemAt(x, y, exclude, physics); + if (topItem != null) { + return topItem.getZ() + (topItem.getBaseItem().allowSit() ? 0 : Item.getCurrentHeight(topItem)); + } + + return height; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomLayout.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomLayout.java index 531f5704..29156355 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomLayout.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomLayout.java @@ -54,6 +54,25 @@ public class RoomLayout { } } + public RoomLayout(RoomManager.RoomLayoutData data, Room room) { + this.room = room; + try { + this.name = data.name; + this.doorX = (short) data.doorX; + this.doorY = (short) data.doorY; + + this.doorDirection = data.doorDir; + this.heightmap = data.heightmap; + + this.parse(); + this.pathfinder = new PathfinderImpl(this.room, MAXIMUM_STEP_HEIGHT, + Emulator.getConfig().getBoolean("pathfinder.step.allow.falling", true), + Emulator.getConfig().getBoolean("pathfinder.retro-style.diagonals", false)); + } catch (Exception e) { + LOGGER.error("Caught exception", e); + } + } + public static boolean squareInSquare(Rectangle outerSquare, Rectangle innerSquare) { if (outerSquare.x > innerSquare.x) { return false; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java index 0b652170..a9699d0f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java @@ -34,12 +34,15 @@ import com.eu.habbo.messages.outgoing.polls.PollStartComposer; import com.eu.habbo.messages.outgoing.polls.infobus.SimplePollAnswersComposer; import com.eu.habbo.messages.outgoing.polls.infobus.SimplePollStartComposer; import com.eu.habbo.messages.outgoing.rooms.*; +import com.eu.habbo.messages.outgoing.rooms.items.ConfInvisStateComposer; +import com.eu.habbo.messages.outgoing.rooms.items.HanditemBlockStateComposer; import com.eu.habbo.messages.outgoing.rooms.items.RoomFloorItemsComposer; import com.eu.habbo.messages.outgoing.rooms.items.RoomWallItemsComposer; import com.eu.habbo.messages.outgoing.rooms.pets.RoomPetComposer; import com.eu.habbo.messages.outgoing.rooms.promotions.RoomPromotionMessageComposer; import com.eu.habbo.messages.outgoing.rooms.users.*; import com.eu.habbo.messages.outgoing.users.MutedWhisperComposer; +import com.eu.habbo.messages.outgoing.users.UserBadgesComposer; import com.eu.habbo.plugin.events.navigator.NavigatorRoomCreatedEvent; import com.eu.habbo.plugin.events.rooms.RoomFloorItemsLoadEvent; import com.eu.habbo.plugin.events.rooms.RoomUncachedEvent; @@ -71,14 +74,18 @@ public class RoomManager { public static boolean SHOW_PUBLIC_IN_POPULAR_TAB = false; private final THashMap roomCategories; private final List mapNames; + private final ConcurrentHashMap layoutCache; private final ConcurrentHashMap activeRooms; + private final ConcurrentHashMap> roomsByOwner; private final ArrayList> gameTypes; public RoomManager() { long millis = System.currentTimeMillis(); this.roomCategories = new THashMap<>(); this.mapNames = new ArrayList<>(); + this.layoutCache = new ConcurrentHashMap<>(); this.activeRooms = new ConcurrentHashMap<>(); + this.roomsByOwner = new ConcurrentHashMap<>(); this.loadRoomCategories(); this.loadRoomModels(); @@ -95,11 +102,28 @@ public class RoomManager { LOGGER.info("Room Manager -> Loaded! ({} MS)", System.currentTimeMillis() - millis); } + private void trackRoomOwner(Room room) { + this.roomsByOwner.computeIfAbsent(room.getOwnerId(), k -> ConcurrentHashMap.newKeySet()).add(room.getId()); + } + + private void untrackRoomOwner(Room room) { + Set rooms = this.roomsByOwner.get(room.getOwnerId()); + if (rooms != null) { + rooms.remove(room.getId()); + if (rooms.isEmpty()) { + this.roomsByOwner.remove(room.getOwnerId()); + } + } + } + public void loadRoomModels() { this.mapNames.clear(); + this.layoutCache.clear(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM room_models")) { while (set.next()) { - this.mapNames.add(set.getString("name")); + String name = set.getString("name"); + this.mapNames.add(name); + this.layoutCache.put(name, new RoomLayoutData(set)); } } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); @@ -143,6 +167,7 @@ public class RoomManager { Room room = new Room(set); room.preventUncaching = true; this.activeRooms.put(set.getInt("id"), room); + this.trackRoomOwner(room); } } } catch (SQLException e) { @@ -162,6 +187,7 @@ public class RoomManager { if (room == null) { room = new Room(set); this.activeRooms.put(set.getInt("id"), room); + this.trackRoomOwner(room); } if (!rooms.containsKey(set.getInt("category"))) { @@ -179,12 +205,7 @@ public class RoomManager { } public RoomCategory getCategory(int id) { - for (RoomCategory category : this.roomCategories.values()) { - if (category.getId() == id) - return category; - } - - return null; + return this.roomCategories.get(id); } public RoomCategory getCategory(String name) { @@ -220,15 +241,8 @@ public class RoomManager { } public boolean hasCategory(int categoryId, Habbo habbo) { - for (RoomCategory category : this.roomCategories.values()) { - if (category.getId() == categoryId) { - if (category.getMinRank() <= habbo.getHabboInfo().getRank().getId()) { - return true; - } - } - } - - return false; + RoomCategory category = this.roomCategories.get(categoryId); + return category != null && category.getMinRank() <= habbo.getHabboInfo().getRank().getId(); } public THashMap getRoomCategories() { @@ -292,7 +306,7 @@ public class RoomManager { /** * Loads a room, optionally loading its data. * If the room is already being loaded in the background, this will wait for that to complete. - * + * * @param id The room ID * @param loadData Whether to load room data (items, bots, pets, etc.) * @return The loaded room, or null if not found @@ -333,6 +347,7 @@ public class RoomManager { if (room != null) { this.activeRooms.put(room.getId(), room); + this.trackRoomOwner(room); } } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); @@ -380,8 +395,11 @@ public class RoomManager { statement.setInt(1, habbo.getHabboInfo().getId()); try (ResultSet set = statement.executeQuery()) { while (set.next()) { - if (!this.activeRooms.containsKey(set.getInt("id"))) - this.activeRooms.put(set.getInt("id"), new Room(set)); + if (!this.activeRooms.containsKey(set.getInt("id"))) { + Room room = new Room(set); + this.activeRooms.put(room.getId(), room); + this.trackRoomOwner(room); + } } } } catch (SQLException e) { @@ -402,22 +420,31 @@ public class RoomManager { continue; room.dispose(); + this.untrackRoomOwner(room); this.activeRooms.remove(room.getId()); } } public void clearInactiveRooms() { THashSet roomsToDispose = new THashSet<>(); - for (Room room : this.activeRooms.values()) { - if (!room.isPublicRoom() && !room.isStaffPromotedRoom() && !Emulator.getGameServer().getGameClientManager().containsHabbo(room.getOwnerId()) && room.isPreLoaded()) { - roomsToDispose.add(room); + for (Map.Entry> entry : this.roomsByOwner.entrySet()) { + int ownerId = entry.getKey(); + if (!Emulator.getGameServer().getGameClientManager().containsHabbo(ownerId)) { + for (int roomId : entry.getValue()) { + Room room = this.activeRooms.get(roomId); + if (room != null && !room.isPublicRoom() && !room.isStaffPromotedRoom() && room.isPreLoaded()) { + roomsToDispose.add(room); + } + } } } for (Room room : roomsToDispose) { room.dispose(); - if (room.getUserCount() == 0) + if (room.getUserCount() == 0) { + this.untrackRoomOwner(room); this.activeRooms.remove(room.getId()); + } } } @@ -426,6 +453,12 @@ public class RoomManager { } public RoomLayout loadLayout(String name, Room room) { + RoomLayoutData cached = this.layoutCache.get(name); + if (cached != null) { + return new RoomLayout(cached, room); + } + + // Fallback to DB if not in cache (should not happen for standard models) RoomLayout layout = null; try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM room_models WHERE name = ? LIMIT 1")) { statement.setString(1, name); @@ -446,6 +479,7 @@ public class RoomManager { } public void uncacheRoom(Room room) { + this.untrackRoomOwner(room); this.activeRooms.remove(room.getId()); } @@ -464,7 +498,7 @@ public class RoomManager { h.getClient().sendResponse(new RoomScoreComposer(room.getScore(), !this.hasVotedForRoom(h, room))); } - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO room_votes VALUES (?, ?)")) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO room_votes (user_id, room_id) VALUES (?, ?)")) { statement.setInt(1, habbo.getHabboInfo().getId()); statement.setInt(2, room.getId()); statement.execute(); @@ -499,14 +533,18 @@ public class RoomManager { } public void enterRoom(Habbo habbo, int roomId, String password) { - this.enterRoom(habbo, roomId, password, false, null); + this.enterRoom(habbo, roomId, password, false, null, false); } public void enterRoom(Habbo habbo, int roomId, String password, boolean overrideChecks) { - this.enterRoom(habbo, roomId, password, overrideChecks, null); + this.enterRoom(habbo, roomId, password, overrideChecks, null, false); } public void enterRoom(Habbo habbo, int roomId, String password, boolean overrideChecks, RoomTile doorLocation) { + this.enterRoom(habbo, roomId, password, overrideChecks, doorLocation, false); + } + + public void enterRoom(Habbo habbo, int roomId, String password, boolean overrideChecks, RoomTile doorLocation, boolean isReconnectSpawn) { Room room = this.loadRoom(roomId, true); if (room == null) @@ -531,6 +569,18 @@ public class RoomManager { return; } + if (room.isBuildersClubTrialLocked() + && habbo.getHabboInfo().getId() != room.getOwnerId() + && !overrideChecks + && !habbo.hasPermission(Permission.ACC_ANYROOMOWNER) + && !habbo.hasPermission(Permission.ACC_ENTERANYROOM)) { + BuildersClubRoomSupport.sendVisitDeniedOwnerBubble(room.getOwnerId(), habbo.getHabboInfo().getUsername()); + BuildersClubRoomSupport.sendVisitDeniedVisitorAlert(habbo.getHabboInfo().getId()); + habbo.getClient().sendResponse(new HotelViewComposer()); + habbo.getHabboInfo().setLoadingRoom(0); + return; + } + if (habbo.getHabboInfo().getRoomQueueId() != roomId) { Room queRoom = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId); @@ -547,7 +597,7 @@ public class RoomManager { room.hasRights(habbo) || (room.getState().equals(RoomState.INVISIBLE) && room.hasRights(habbo)) || (room.hasGuild() && room.getGuildRightLevel(habbo).isGreaterThan(RoomRightLevels.GUILD_RIGHTS))) { - this.openRoom(habbo, room, doorLocation); + this.openRoom(habbo, room, doorLocation, isReconnectSpawn); } else if (room.getState() == RoomState.LOCKED) { boolean rightsFound = false; @@ -572,7 +622,7 @@ public class RoomManager { room.addToQueue(habbo); } else if (room.getState() == RoomState.PASSWORD) { if (room.getPassword().equalsIgnoreCase(password)) - this.openRoom(habbo, room, doorLocation); + this.openRoom(habbo, room, doorLocation, isReconnectSpawn); else { habbo.getClient().sendResponse(new GenericErrorMessagesComposer(GenericErrorMessagesComposer.WRONG_PASSWORD_USED)); habbo.getClient().sendResponse(new HotelViewComposer()); @@ -585,6 +635,10 @@ public class RoomManager { } void openRoom(Habbo habbo, Room room, RoomTile doorLocation) { + this.openRoom(habbo, room, doorLocation, false); + } + + void openRoom(Habbo habbo, Room room, RoomTile doorLocation, boolean isReconnectSpawn) { if (room == null || room.getLayout() == null) return; @@ -614,6 +668,8 @@ public class RoomManager { } habbo.setRoomUnit(new RoomUnit()); + habbo.getHabboInfo().setRoomEntryMethod("door"); + habbo.getHabboInfo().setRoomEntryTeleportId(0); habbo.getRoomUnit().clearStatus(); if (habbo.getRoomUnit().getCurrentLocation() == null) { @@ -623,11 +679,19 @@ public class RoomManager { if (doorLocation == null) { habbo.getRoomUnit().setBodyRotation(RoomUserRotation.values()[room.getLayout().getDoorDirection()]); habbo.getRoomUnit().setHeadRotation(RoomUserRotation.values()[room.getLayout().getDoorDirection()]); + } else if (isReconnectSpawn) { + // Reconnect spawn: place at tile but keep normal room behavior + // (user can still leave by door, no teleport flags) + habbo.getRoomUnit().setBodyRotation(RoomUserRotation.values()[room.getLayout().getDoorDirection()]); + habbo.getRoomUnit().setHeadRotation(RoomUserRotation.values()[room.getLayout().getDoorDirection()]); } else { + // Furniture teleport spawn habbo.getRoomUnit().setCanLeaveRoomByDoor(false); habbo.getRoomUnit().isTeleporting = true; + habbo.getHabboInfo().setRoomEntryMethod("teleport"); HabboItem topItem = room.getTopItemAt(doorLocation.x, doorLocation.y); if (topItem != null) { + habbo.getHabboInfo().setRoomEntryTeleportId(topItem.getId()); habbo.getRoomUnit().setRotation(RoomUserRotation.values()[topItem.getRotation()]); } } @@ -725,11 +789,24 @@ public class RoomManager { habbo.getRoomUnit().setHeadRotation(RoomUserRotation.values()[room.getLayout().getDoorDirection()]); } + if (habbo.getRoomUnit().getCurrentLocation() == null) { + LOGGER.warn("Failed to resolve a valid door tile for room {} ({}) while {} was entering; sending user back to hotel view", + room.getId(), room.getName(), habbo.getHabboInfo().getUsername()); + habbo.getHabboInfo().setLoadingRoom(0); + habbo.getHabboInfo().setCurrentRoom(null); + habbo.getClient().sendResponse(new HotelViewComposer()); + return; + } + habbo.getRoomUnit().setPathFinderRoom(room); habbo.getRoomUnit().resetIdleTimer(); habbo.getRoomUnit().setInvisible(false); room.addHabbo(habbo); + BuildersClubRoomSupport.sendCurrentRoomPlacementStatus(room); + room.getUserVariableManager().restorePermanentAssignments(habbo); + + habbo.getClient().sendResponse(new UserBadgesComposer(habbo.getInventory().getBadgesComponent().getWearingBadges(), habbo.getHabboInfo().getId())); List habbos = new ArrayList<>(); if (!room.getCurrentHabbos().isEmpty()) { @@ -832,6 +909,10 @@ public class RoomManager { floorItems.clear(); } + habbo.getClient().sendResponse(new ConfInvisStateComposer(room).compose()); + RoomAreaHideSupport.sendState(room, habbo.getClient()); + habbo.getClient().sendResponse(new HanditemBlockStateComposer(room).compose()); + if (!room.getCurrentPets().isEmpty()) { habbo.getClient().sendResponse(new RoomPetComposer(room.getCurrentPets())); for (Pet pet : room.getCurrentPets().valueCollection()) { @@ -925,6 +1006,20 @@ public class RoomManager { } } + habbo.getClient().sendResponse(new com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomSettingsComposer( + room.isYoutubeEnabled()).compose()); + + if (!room.getYoutubeCurrentVideo().isEmpty()) { + habbo.getClient().sendResponse(new com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomBroadcastComposer( + room.getYoutubeCurrentVideo(), + room.getYoutubeSenderName(), + room.getYoutubePlaylist()).compose()); + } + if (!room.getYoutubeWatchers().isEmpty()) { + habbo.getClient().sendResponse(new com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomWatchersComposer( + room.getYoutubeWatchers()).compose()); + } + WiredManager.triggerUserEntersRoom(room, habbo.getRoomUnit()); room.habboEntered(habbo); @@ -958,6 +1053,7 @@ public class RoomManager { this.logExit(habbo); room.removeHabbo(habbo, true); + BuildersClubRoomSupport.sendCurrentRoomPlacementStatus(room); if (redirectToHotelView) { habbo.getClient().sendResponse(new HotelViewComposer()); @@ -1123,6 +1219,7 @@ public class RoomManager { Room r = new Room(set); rooms.add(r); this.activeRooms.put(r.getId(), r); + this.trackRoomOwner(r); } } } catch (SQLException e) { @@ -1183,6 +1280,7 @@ public class RoomManager { rooms.add(r); this.activeRooms.put(r.getId(), r); + this.trackRoomOwner(r); } } } catch (SQLException e) { @@ -1246,6 +1344,7 @@ public class RoomManager { room = new Room(set); this.activeRooms.put(room.getId(), room); + this.trackRoomOwner(room); } rooms.add(room); @@ -1487,6 +1586,7 @@ public class RoomManager { room.dispose(); } + this.roomsByOwner.clear(); this.activeRooms.clear(); LOGGER.info("Room Manager -> Disposed!"); @@ -1581,4 +1681,24 @@ public class RoomManager { this.duration = duration; } } + + /** + * Cached layout data from room_models to avoid repeated DB queries. + * The raw data is shared; each Room gets its own RoomLayout instance. + */ + static class RoomLayoutData { + final String name; + final int doorX; + final int doorY; + final int doorDir; + final String heightmap; + + RoomLayoutData(ResultSet set) throws SQLException { + this.name = set.getString("name"); + this.doorX = set.getInt("door_x"); + this.doorY = set.getInt("door_y"); + this.doorDir = set.getInt("door_dir"); + this.heightmap = set.getString("heightmap"); + } + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomMessagingManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomMessagingManager.java index 10c8173c..5807cdec 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomMessagingManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomMessagingManager.java @@ -4,6 +4,9 @@ import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer; +import java.util.ArrayList; +import java.util.Collection; + /** * Manages all messaging and communication within a room. * Handles sending messages to Habbos, pet/bot chat, and alerts. @@ -30,6 +33,34 @@ public class RoomMessagingManager { } } + public void sendComposers(Collection messages) { + if (messages == null || messages.isEmpty()) { + return; + } + + ArrayList responses = new ArrayList<>(); + + for (ServerMessage message : messages) { + if (message == null) { + continue; + } + + responses.add(message); + } + + if (responses.isEmpty()) { + return; + } + + for (Habbo habbo : this.room.getHabbos()) { + if (habbo.getClient() == null) { + continue; + } + + habbo.getClient().sendResponses(new ArrayList<>(responses)); + } + } + /** * Sends a message to all Habbos with rights in the room. */ diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomQueueSpeedControlSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomQueueSpeedControlSupport.java new file mode 100644 index 00000000..18019c9f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomQueueSpeedControlSupport.java @@ -0,0 +1,77 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.habbohotel.items.interactions.InteractionQueueSpeedControl; +import com.eu.habbo.habbohotel.items.interactions.InteractionRoller; +import com.eu.habbo.habbohotel.users.HabboItem; + +public final class RoomQueueSpeedControlSupport { + private static final String CONTROLLER_INTERACTION = "wf_conf_queue_speed"; + + private RoomQueueSpeedControlSupport() { + } + + public static Integer getEffectiveRollerSpeed(Room room) { + HabboItem controller = getControllerItem(room); + return controller != null ? InteractionQueueSpeedControl.toRollerSpeed(controller.getExtradata()) : null; + } + + public static int getEffectiveRollerIntervalMs(Room room) { + Integer effectiveRollerSpeed = getEffectiveRollerSpeed(room); + + if (effectiveRollerSpeed != null) { + return toRollerIntervalMs(effectiveRollerSpeed); + } + + if (room == null) { + return InteractionRoller.DELAY; + } + + return toRollerIntervalMs(room.getRollerSpeed()); + } + + private static int toRollerIntervalMs(int rollerSpeed) { + if (rollerSpeed < 0) { + return InteractionRoller.DELAY; + } + + return (rollerSpeed + 1) * 500; + } + + private static boolean isControllerItem(HabboItem item) { + if (item == null || item.getBaseItem() == null) { + return false; + } + + if (item instanceof InteractionQueueSpeedControl) { + return true; + } + + if (item.getBaseItem().getInteractionType() == null) { + return false; + } + + String interactionName = item.getBaseItem().getInteractionType().getName(); + + return interactionName != null && interactionName.equalsIgnoreCase(CONTROLLER_INTERACTION); + } + + private static HabboItem getControllerItem(Room room) { + if (room == null) { + return null; + } + + for (HabboItem item : room.getFloorItems()) { + if (!isControllerItem(item)) { + continue; + } + + if (item instanceof InteractionQueueSpeedControl) { + ((InteractionQueueSpeedControl) item).ensureAnimationLoop(room); + } + + return item; + } + + return null; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRightsManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRightsManager.java index f21b5289..c9eb266d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRightsManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRightsManager.java @@ -1,7 +1,10 @@ package com.eu.habbo.habbohotel.rooms; import com.eu.habbo.Emulator; +import com.eu.habbo.database.SqlQueries; import com.eu.habbo.habbohotel.guilds.Guild; +import com.eu.habbo.habbohotel.guilds.GuildMember; +import com.eu.habbo.habbohotel.guilds.GuildRank; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.outgoing.rooms.RoomAddRightsListComposer; @@ -14,7 +17,6 @@ import com.eu.habbo.habbohotel.messenger.MessengerBuddy; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.plugin.events.users.UserRightsTakenEvent; import gnu.trove.list.array.TIntArrayList; -import gnu.trove.map.hash.THashMap; import gnu.trove.map.hash.TIntIntHashMap; import gnu.trove.map.hash.TIntObjectHashMap; import org.slf4j.Logger; @@ -24,6 +26,9 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; /** * Manages room rights, bans, and mutes. @@ -90,11 +95,16 @@ public class RoomRightsManager { */ public RoomRightLevels getGuildRightLevel(Habbo habbo) { int guildId = this.room.getGuildId(); - if (guildId > 0 && habbo.getHabboStats().hasGuild(guildId)) { + if (guildId > 0 && habbo != null && habbo.getHabboInfo() != null) { Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId); - if (Emulator.getGameEnvironment().getGuildManager().getOnlyAdmins(guild) - .get(habbo.getHabboInfo().getId()) != null) { + if (guild == null) { + return RoomRightLevels.NONE; + } + + GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild.getId(), habbo.getHabboInfo().getId()); + + if ((member != null) && (member.getRank() == GuildRank.ADMIN || member.getRank() == GuildRank.OWNER)) { return RoomRightLevels.GUILD_ADMIN; } @@ -149,13 +159,11 @@ public class RoomRightsManager { } if (this.rights.add(userId)) { - try (Connection connection = Emulator.getDatabase().getDataSource() - .getConnection(); PreparedStatement statement = connection.prepareStatement( - "INSERT INTO room_rights VALUES (?, ?)")) { - statement.setInt(1, this.room.getId()); - statement.setInt(2, userId); - statement.execute(); - } catch (SQLException e) { + try { + SqlQueries.update( + "INSERT INTO room_rights (room_id, user_id) VALUES (?, ?)", + this.room.getId(), userId); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); } } @@ -196,13 +204,11 @@ public class RoomRightsManager { this.room.sendComposer(new RoomRemoveRightsListComposer(this.room, userId).compose()); if (this.rights.remove(userId)) { - try (Connection connection = Emulator.getDatabase().getDataSource() - .getConnection(); PreparedStatement statement = connection.prepareStatement( - "DELETE FROM room_rights WHERE room_id = ? AND user_id = ?")) { - statement.setInt(1, this.room.getId()); - statement.setInt(2, userId); - statement.execute(); - } catch (SQLException e) { + try { + SqlQueries.update( + "DELETE FROM room_rights WHERE room_id = ? AND user_id = ?", + this.room.getId(), userId); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); } } @@ -225,12 +231,9 @@ public class RoomRightsManager { this.rights.clear(); - try (Connection connection = Emulator.getDatabase().getDataSource() - .getConnection(); PreparedStatement statement = connection.prepareStatement( - "DELETE FROM room_rights WHERE room_id = ?")) { - statement.setInt(1, this.room.getId()); - statement.execute(); - } catch (SQLException e) { + try { + SqlQueries.update("DELETE FROM room_rights WHERE room_id = ?", this.room.getId()); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); } @@ -288,25 +291,22 @@ public class RoomRightsManager { /** * Gets all users with rights in the room. */ - public THashMap getUsersWithRights() { - THashMap rightsMap = new THashMap<>(); - - if (!this.rights.isEmpty()) { - try (Connection connection = Emulator.getDatabase().getDataSource() - .getConnection(); PreparedStatement statement = connection.prepareStatement( - "SELECT users.username AS username, users.id as user_id FROM room_rights INNER JOIN users ON room_rights.user_id = users.id WHERE room_id = ?")) { - statement.setInt(1, this.room.getId()); - try (ResultSet set = statement.executeQuery()) { - while (set.next()) { - rightsMap.put(set.getInt("user_id"), set.getString("username")); - } - } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } + public Map getUsersWithRights() { + if (this.rights.isEmpty()) { + return Collections.emptyMap(); } - return rightsMap; + try { + return SqlQueries.query( + "SELECT users.username AS username, users.id as user_id FROM room_rights INNER JOIN users ON room_rights.user_id = users.id WHERE room_id = ?", + rs -> Map.entry(rs.getInt("user_id"), rs.getString("username")), + this.room.getId()) + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> b)); + } catch (SqlQueries.DataAccessException e) { + LOGGER.error("Caught SQL exception", e); + return Collections.emptyMap(); + } } /** diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRollerManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRollerManager.java index 9d564ece..318e6f78 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRollerManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRollerManager.java @@ -354,7 +354,7 @@ public class RoomRollerManager { LOGGER.error("Caught exception", e); } } - }, this.room.getRollerSpeed() == 0 ? 250 : InteractionRoller.DELAY); + }, RoomQueueSpeedControlSupport.getEffectiveRollerIntervalMs(this.room)); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java index 6d00aafe..dcbdfd11 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java @@ -30,7 +30,9 @@ import gnu.trove.set.hash.THashSet; import java.awt.*; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -346,7 +348,7 @@ public class RoomSpecialTypes { public static final int MAX_SENDERS_PER_RECEIVER = 5; public boolean isSignalSenderLimitReached() { - Set existing = this.wiredEffects.get(WiredEffectType.SEND_SIGNAL); + Set existing = this.getSignalSenders(); return existing != null && existing.size() >= MAX_SIGNAL_SENDERS_PER_ROOM; } @@ -356,7 +358,7 @@ public class RoomSpecialTypes { } public int countSendersTargetingReceiver(int receiverItemId, InteractionWiredEffect excludeSender) { - Set senders = this.wiredEffects.get(WiredEffectType.SEND_SIGNAL); + Set senders = this.getSignalSenders(); if (senders == null) return 0; int count = 0; @@ -376,6 +378,95 @@ public class RoomSpecialTypes { return countSendersTargetingReceiver(receiverItemId, null); } + public int countSendersTargetingAnyReceiver(Collection receiverItemIds, InteractionWiredEffect excludeSender) { + if (receiverItemIds == null || receiverItemIds.isEmpty()) { + return 0; + } + + Set senders = this.getSignalSenders(); + if (senders == null) { + return 0; + } + + Set uniqueSenderIds = new HashSet<>(); + + for (InteractionWiredEffect effect : senders) { + if (excludeSender != null && effect.getId() == excludeSender.getId()) continue; + if (!(effect instanceof WiredEffectSendSignal)) continue; + + WiredEffectSendSignal sender = (WiredEffectSendSignal) effect; + for (Integer receiverItemId : receiverItemIds) { + if (receiverItemId == null) continue; + if (!sender.hasPickedItem(receiverItemId)) continue; + + uniqueSenderIds.add(effect.getId()); + break; + } + } + + return uniqueSenderIds.size(); + } + + public int countSendersTargetingAnyReceiver(Collection receiverItemIds) { + return countSendersTargetingAnyReceiver(receiverItemIds, null); + } + + public boolean unlinkSignalAntennaReferences(int antennaItemId) { + if (antennaItemId <= 0) { + return false; + } + + boolean changed = false; + + THashSet receivers = this.getTriggers(WiredTriggerType.RECEIVE_SIGNAL); + for (InteractionWiredTrigger trigger : receivers) { + if (!(trigger instanceof com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerReceiveSignal receiver)) { + continue; + } + + if (!receiver.unlinkAntenna(antennaItemId)) { + continue; + } + + changed = true; + Emulator.getThreading().run(receiver); + } + + Set senders = this.getSignalSenders(); + if (senders != null) { + for (InteractionWiredEffect effect : senders) { + if (!(effect instanceof WiredEffectSendSignal sender)) { + continue; + } + + if (!sender.unlinkAntenna(antennaItemId)) { + continue; + } + + changed = true; + Emulator.getThreading().run(sender); + } + } + + return changed; + } + + private Set getSignalSenders() { + Set senders = new HashSet<>(); + + Set standardSenders = this.wiredEffects.get(WiredEffectType.SEND_SIGNAL); + if (standardSenders != null) { + senders.addAll(standardSenders); + } + + Set negativeSenders = this.wiredEffects.get(WiredEffectType.NEG_SEND_SIGNAL); + if (negativeSenders != null) { + senders.addAll(negativeSenders); + } + + return senders.isEmpty() ? null : senders; + } + public void addTrigger(InteractionWiredTrigger trigger) { // Add to type-based index this.wiredTriggers.computeIfAbsent(trigger.getType(), k -> ConcurrentHashMap.newKeySet()) @@ -686,6 +777,15 @@ public class RoomSpecialTypes { return result; } + /** + * Finds a wired extra by its item ID. + * @param itemId The item ID to search for + * @return The extra if found, null otherwise + */ + public InteractionWiredExtra getExtra(int itemId) { + return this.wiredExtras.get(itemId); + } + /** * Gets all wired extras at specific coordinates using spatial index. * @param x The X coordinate diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java index 828abb80..e585b675 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java @@ -4,6 +4,7 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.bots.Bot; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionStackHelper; +import com.eu.habbo.habbohotel.items.interactions.InteractionStackWalkHelper; import com.eu.habbo.habbohotel.items.interactions.InteractionTileWalkMagic; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.plugin.events.furniture.FurnitureStackHeightEvent; @@ -149,7 +150,7 @@ public class RoomTileManager { result = overriddenState; } - if (this.room.getItemManager().getItemsAt(tile).stream().anyMatch(i -> i instanceof InteractionTileWalkMagic)) { + if (this.room.getItemManager().getItemsAt(tile).stream().anyMatch(i -> i instanceof InteractionTileWalkMagic || i instanceof InteractionStackWalkHelper)) { result = RoomTileState.OPEN; } @@ -211,14 +212,20 @@ public class RoomTileManager { boolean canStack = true; THashSet stackHelpers = this.room.getItemManager().getItemsAt(InteractionStackHelper.class, x, y); + stackHelpers.addAll(this.room.getItemManager().getItemsAt(InteractionStackWalkHelper.class, x, y)); stackHelpers.addAll(this.room.getItemManager().getItemsAt(InteractionTileWalkMagic.class, x, y)); if (stackHelpers.size() > 0) { + double helperHeight = Double.NEGATIVE_INFINITY; for (HabboItem item : stackHelpers) { if (item == exclude) { continue; } - return calculateHeightmap ? item.getZ() * 256.0D : item.getZ(); + helperHeight = Math.max(helperHeight, item.getZ()); + } + + if (helperHeight != Double.NEGATIVE_INFINITY) { + return calculateHeightmap ? helperHeight * 256.0D : helperHeight; } } @@ -425,6 +432,10 @@ public class RoomTileManager { HabboItem topItem = null; boolean canWalk = true; THashSet items = this.room.getItemManager().getItemsAt(roomTile); + if (items != null && items.stream().anyMatch(item -> item instanceof InteractionTileWalkMagic || item instanceof InteractionStackWalkHelper)) { + return true; + } + if (items != null) { for (HabboItem item : items) { if (topItem == null) { @@ -517,17 +528,43 @@ public class RoomTileManager { /** * Loads the heightmap for the room. + * Only updates tiles that have items on them (+ door tile) instead of all tiles, + * using getTilesAt() to correctly handle rotated multi-tile furniture. */ public void loadHeightmap() { RoomLayout layout = this.room.getLayout(); if (layout != null) { - for (short x = 0; x < layout.getMapSizeX(); x++) { - for (short y = 0; y < layout.getMapSizeY(); y++) { - RoomTile tile = layout.getTile(x, y); - if (tile != null) { - this.updateTile(tile); - } + THashSet floorItems = this.room.getFloorItems(); + + if (floorItems.isEmpty()) { + // No items - only update door tile + RoomTile doorTile = layout.getDoorTile(); + if (doorTile != null) { + this.updateTile(doorTile); } + return; + } + + // Collect unique tiles occupied by items (handles rotation) + THashSet tilesToUpdate = new THashSet<>(); + for (HabboItem item : floorItems) { + RoomTile baseTile = layout.getTile(item.getX(), item.getY()); + if (baseTile != null) { + tilesToUpdate.addAll(layout.getTilesAt(baseTile, + item.getBaseItem().getWidth(), + item.getBaseItem().getLength(), + item.getRotation())); + } + } + + // Always include door tile + RoomTile doorTile = layout.getDoorTile(); + if (doorTile != null) { + tilesToUpdate.add(doorTile); + } + + for (RoomTile tile : tilesToUpdate) { + this.updateTile(tile); } } else { LOGGER.error("Unknown Room Layout for Room (ID: {})", this.room.getId()); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java index e7a08aea..7b9326e0 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java @@ -4,11 +4,14 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.bots.Bot; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionTileWalkMagic; +import com.eu.habbo.habbohotel.items.interactions.InteractionStackWalkHelper; import com.eu.habbo.habbohotel.items.interactions.InteractionWater; import com.eu.habbo.habbohotel.items.interactions.InteractionWaterItem; import com.eu.habbo.habbohotel.items.interactions.interfaces.ConditionalGate; import com.eu.habbo.habbohotel.pets.Pet; import com.eu.habbo.habbohotel.pets.RideablePet; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; +import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; import com.eu.habbo.habbohotel.users.DanceType; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboItem; @@ -72,6 +75,7 @@ public class RoomUnit { private int handItem; private long handItemTimestamp; private long lastRollerTime; + private long moveStatusTimestamp; private int walkTimeOut; private int effectId; private int effectEndTimestamp; @@ -102,6 +106,7 @@ public class RoomUnit { this.goalLocation = null; this.startLocation = this.currentLocation; this.inRoom = false; + this.moveStatusTimestamp = 0L; this.status.clear(); @@ -321,7 +326,7 @@ public class RoomUnit { } Optional stackHelper = room.getItemsAt(next).stream() - .filter(i -> i instanceof InteractionTileWalkMagic).findAny(); + .filter(i -> i instanceof InteractionTileWalkMagic || i instanceof InteractionStackWalkHelper).findAny(); if (stackHelper.isPresent()) { zHeight = stackHelper.get().getZ(); } @@ -406,11 +411,11 @@ public class RoomUnit { } public short getX() { - return this.currentLocation.x; + return this.currentLocation == null ? 0 : this.currentLocation.x; } public short getY() { - return this.currentLocation.y; + return this.currentLocation == null ? 0 : this.currentLocation.y; } public double getZ() { @@ -593,6 +598,7 @@ public class RoomUnit { } public boolean isAtGoal() { + if (this.currentLocation == null) return true; return this.currentLocation.equals(this.goalLocation); } @@ -609,11 +615,20 @@ public class RoomUnit { } public void removeStatus(RoomUnitStatus key) { + if (key == RoomUnitStatus.MOVE) { + this.moveStatusTimestamp = 0L; + } this.status.remove(key); } public void setStatus(RoomUnitStatus key, String value) { if (key != null && value != null) { + if (key == RoomUnitStatus.MOVE) { + this.moveStatusTimestamp = System.currentTimeMillis(); + WiredMoveCarryHelper.clearStatusComposerSuppression(this); + WiredUserMovementHelper.clearStatusComposerSuppression(this); + } + this.status.put(key, value); } } @@ -623,6 +638,7 @@ public class RoomUnit { } public void clearStatus() { + this.moveStatusTimestamp = 0L; this.status.clear(); } @@ -650,6 +666,10 @@ public class RoomUnit { this.lastRollerTime = lastRollerTime; } + public long getMoveStatusTimestamp() { + return this.moveStatusTimestamp; + } + /** * Checks if enough time has passed since the last roller movement to allow rolling again. * This prevents desync issues where the client hasn't finished the roller animation. diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitManager.java index 9ac9c726..e7707611 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnitManager.java @@ -12,7 +12,11 @@ import com.eu.habbo.habbohotel.users.DanceType; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboGender; import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredUserActionType; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; +import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; import com.eu.habbo.messages.outgoing.generic.alerts.GenericErrorMessagesComposer; import com.eu.habbo.messages.outgoing.inventory.AddPetComposer; import com.eu.habbo.messages.outgoing.rooms.pets.RoomPetComposer; @@ -217,16 +221,25 @@ public class RoomUnitManager { return; } + if (habbo.getRoomUnit() != null) { + WiredManager.triggerUserLeavesRoom(this.room, habbo.getRoomUnit()); + if (WiredFreezeUtil.isFrozen(habbo.getRoomUnit())) { + WiredFreezeUtil.unfreeze(this.room, habbo.getRoomUnit()); + } + } + if (habbo.getRoomUnit() != null && habbo.getRoomUnit().getCurrentLocation() != null) { habbo.getRoomUnit().getCurrentLocation().removeUnit(habbo.getRoomUnit()); } - synchronized (this.room.roomUnitLock) { - this.currentHabbos.remove(habbo.getHabboInfo().getId()); - } + synchronized (this.room.roomUnitLock) { + this.currentHabbos.remove(habbo.getHabboInfo().getId()); + } - if (sendRemovePacket && habbo.getRoomUnit() != null && !habbo.getRoomUnit().isTeleporting) { - this.room.sendComposer(new RoomUserRemoveComposer(habbo.getRoomUnit()).compose()); + this.room.getUserVariableManager().clearAssignmentsForUser(habbo.getHabboInfo().getId()); + + if (sendRemovePacket && habbo.getRoomUnit() != null && !habbo.getRoomUnit().isTeleporting) { + this.room.sendComposer(new RoomUserRemoveComposer(habbo.getRoomUnit()).compose()); } if (habbo.getRoomUnit().getCurrentLocation() != null) { @@ -351,7 +364,13 @@ public class RoomUnitManager { continue; } + if (WiredMoveCarryHelper.shouldSuppressStatusUpdate(habbo.getRoomUnit()) + || WiredUserMovementHelper.shouldSuppressStatusUpdate(habbo.getRoomUnit())) { + continue; + } + double z = habbo.getRoomUnit().getCurrentLocation().getStackHeight(); + boolean hadLayStatus = habbo.getRoomUnit().hasStatus(RoomUnitStatus.LAY); if (habbo.getRoomUnit().hasStatus(RoomUnitStatus.SIT) || (topItem != null && topItem.getBaseItem().allowSit())) { @@ -413,14 +432,26 @@ public class RoomUnitManager { } habbo.getRoomUnit().statusUpdate(true); + + if (!hadLayStatus && habbo.getRoomUnit().hasStatus(RoomUnitStatus.LAY)) { + WiredManager.triggerUserPerformsAction(this.room, habbo.getRoomUnit(), WiredUserActionType.LAY, -1); + } } if (!habbos.isEmpty()) { THashSet roomUnits = new THashSet<>(); for (Habbo habbo : habbos) { + if (habbo.getRoomUnit() == null + || WiredMoveCarryHelper.shouldSuppressStatusUpdate(habbo.getRoomUnit()) + || WiredUserMovementHelper.shouldSuppressStatusUpdate(habbo.getRoomUnit())) { + continue; + } roomUnits.add(habbo.getRoomUnit()); } - this.room.sendComposer(new RoomUserStatusComposer(roomUnits, true).compose()); + + if (!roomUnits.isEmpty()) { + this.room.sendComposer(new RoomUserStatusComposer(roomUnits, true).compose()); + } } if (topItem != null && topItem.getBaseItem().allowLay()) { @@ -1233,9 +1264,14 @@ public class RoomUnitManager { if (habbo == null || habbo.getRoomUnit() == null) { return; } + + boolean wasIdle = habbo.getRoomUnit().isIdle(); habbo.getRoomUnit().resetIdleTimer(); - this.room.sendComposer(new RoomUnitIdleComposer(habbo.getRoomUnit()).compose()); - WiredManager.triggerUserUnidles(this.room, habbo.getRoomUnit()); + + if (wasIdle) { + this.room.sendComposer(new RoomUnitIdleComposer(habbo.getRoomUnit()).compose()); + WiredManager.triggerUserUnidles(this.room, habbo.getRoomUnit()); + } } /** @@ -1299,6 +1335,8 @@ public class RoomUnitManager { */ public void teleportRoomUnitToLocation(RoomUnit roomUnit, short x, short y, double z) { if (this.room.isLoaded()) { + WiredFreezeUtil.onTeleport(this.room, roomUnit); + RoomTile tile = this.room.getLayout().getTile(x, y); if (z < tile.z) { @@ -1310,6 +1348,7 @@ public class RoomUnitManager { roomUnit.setZ(z); roomUnit.setPreviousLocationZ(z); this.room.updateRoomUnit(roomUnit); + WiredFreezeUtil.restoreWalkState(roomUnit); } } @@ -1414,11 +1453,6 @@ public class RoomUnitManager { } } - // ==================== DISPOSAL ==================== - - /** - * Disposes the unit manager. - */ public void dispose() { this.currentHabbos.clear(); this.currentBots.clear(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUserVariableManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUserVariableManager.java new file mode 100644 index 00000000..a38b3a1b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUserVariableManager.java @@ -0,0 +1,1136 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +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.WiredVariableReferenceSupport; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUserVariable; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredVariableLevelSystemSupport; +import com.eu.habbo.habbohotel.wired.core.WiredVariableTextConnectorSupport; +import com.eu.habbo.messages.outgoing.wired.WiredUserVariablesDataComposer; +import gnu.trove.set.hash.THashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public class RoomUserVariableManager { + private static final Logger LOGGER = LoggerFactory.getLogger(RoomUserVariableManager.class); + + private final Room room; + private final ConcurrentHashMap> activeAssignmentsByUserId; + + public RoomUserVariableManager(Room room) { + this.room = room; + this.activeAssignmentsByUserId = new ConcurrentHashMap<>(); + } + + public void restorePermanentAssignments(Habbo habbo) { + if (habbo == null) { + return; + } + + int userId = habbo.getHabboInfo().getId(); + ConcurrentHashMap restoredAssignments = new ConcurrentHashMap<>(); + List staleDefinitionIds = new ArrayList<>(); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT variable_item_id, value, created_at, updated_at FROM room_user_wired_variables WHERE room_id = ? AND user_id = ?")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, userId); + + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + int definitionItemId = set.getInt("variable_item_id"); + WiredExtraUserVariable definition = this.getDefinition(definitionItemId); + + if (definition == null || !definition.isPermanentAvailability()) { + staleDefinitionIds.add(definitionItemId); + continue; + } + + Integer value = null; + int rawValue = set.getInt("value"); + if (!set.wasNull()) { + value = rawValue; + } + + int createdAt = normalizeTimestamp(set.getInt("created_at"), 0); + int updatedAt = normalizeTimestamp(set.getInt("updated_at"), createdAt); + + restoredAssignments.put(definitionItemId, new VariableAssignment(value, createdAt, updatedAt)); + } + } + } catch (SQLException e) { + LOGGER.error("Failed to restore wired user variables for room {} and user {}", this.room.getId(), userId, e); + } + + if (!staleDefinitionIds.isEmpty()) { + for (Integer definitionItemId : staleDefinitionIds) { + this.deletePersistentAssignment(userId, definitionItemId); + } + } + + if (restoredAssignments.isEmpty()) { + this.activeAssignmentsByUserId.remove(userId); + } else { + this.activeAssignmentsByUserId.put(userId, restoredAssignments); + } + + this.broadcastSnapshot(); + } + + public boolean assignVariable(Habbo habbo, WiredExtraUserVariable definition, Integer value, boolean overrideExisting) { + return definition != null && this.assignVariable(habbo, definition.getId(), value, overrideExisting); + } + + public boolean assignVariable(Habbo habbo, int definitionItemId, Integer value, boolean overrideExisting) { + if (habbo == null || definitionItemId <= 0) { + return false; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return false; + } + + int userId = habbo.getHabboInfo().getId(); + Integer normalizedValue = definitionInfo.hasValue() ? value : null; + boolean hadBefore = this.hasVariable(userId, definitionItemId); + Integer previousValue = (definitionInfo.hasValue() && hadBefore) ? this.getCurrentValue(userId, definitionItemId) : null; + + if (extra instanceof WiredExtraVariableReference) { + boolean changed = WiredVariableReferenceSupport.assignSharedUserVariable((WiredExtraVariableReference) extra, userId, normalizedValue, overrideExisting); + boolean shouldEmit = changed || (definitionInfo.hasValue() && hadBefore && overrideExisting && Objects.equals(previousValue, normalizedValue)); + + if (shouldEmit) { + boolean hasAfter = this.hasVariable(userId, definitionItemId); + Integer currentValue = (definitionInfo.hasValue() && hasAfter) ? this.getCurrentValue(userId, definitionItemId) : null; + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).assignValue(this.room, userId, normalizedValue, overrideExisting); + boolean shouldEmit = changed || (definitionInfo.hasValue() && hadBefore && overrideExisting && Objects.equals(previousValue, normalizedValue)); + + if (shouldEmit) { + boolean hasAfter = this.hasVariable(userId, definitionItemId); + Integer currentValue = (definitionInfo.hasValue() && hasAfter) ? this.getCurrentValue(userId, definitionItemId) : null; + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.computeIfAbsent(userId, key -> new ConcurrentHashMap<>()); + VariableAssignment existingAssignment = assignments.get(definitionItemId); + + if (existingAssignment != null && !overrideExisting) { + return false; + } + + boolean overwritten = existingAssignment != null && overrideExisting; + boolean valueChanged = existingAssignment == null || !Objects.equals(existingAssignment.getValue(), normalizedValue); + boolean changed = overwritten || valueChanged; + + if (existingAssignment == null || overwritten) { + int now = Emulator.getIntUnixTimestamp(); + assignments.put(definitionItemId, new VariableAssignment(normalizedValue, now, now)); + } else if (valueChanged) { + existingAssignment.setValue(normalizedValue, Emulator.getIntUnixTimestamp()); + } + + WiredExtraUserVariable definition = (WiredExtraUserVariable) extra; + + if (definition.isPermanentAvailability()) { + this.upsertPersistentAssignment(userId, definitionItemId, assignments.get(definitionItemId)); + } else { + this.deletePersistentAssignment(userId, definitionItemId); + } + + if (changed) { + if (definition.isSharedAvailability()) { + VariableAssignment assignment = assignments.get(definitionItemId); + if (assignment != null) { + WiredVariableReferenceSupport.cacheSharedUserAssignment(this.room.getId(), definitionItemId, userId, assignment.getValue(), assignment.getCreatedAt(), assignment.getUpdatedAt()); + } + } else { + WiredVariableReferenceSupport.clearSharedUserAssignment(this.room.getId(), definitionItemId, userId); + } + } + + if (changed || (definitionInfo.hasValue() && hadBefore && overrideExisting && Objects.equals(previousValue, normalizedValue))) { + boolean hasAfter = this.hasVariable(userId, definitionItemId); + Integer currentValue = (definitionInfo.hasValue() && hasAfter) ? this.getCurrentValue(userId, definitionItemId) : null; + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + public boolean updateVariableValue(int userId, int definitionItemId, Integer value) { + if (userId <= 0 || definitionItemId <= 0) { + return false; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + + if (definitionInfo == null || !definitionInfo.hasValue() || definitionInfo.isReadOnly()) { + return false; + } + + boolean hadBefore = this.hasVariable(userId, definitionItemId); + Integer previousValue = hadBefore ? this.getCurrentValue(userId, definitionItemId) : null; + + if (extra instanceof WiredExtraVariableReference) { + boolean changed = WiredVariableReferenceSupport.updateSharedUserVariable((WiredExtraVariableReference) extra, userId, value); + boolean shouldEmit = changed || (hadBefore && Objects.equals(previousValue, value)); + + if (shouldEmit) { + boolean hasAfter = this.hasVariable(userId, definitionItemId); + Integer currentValue = hasAfter ? this.getCurrentValue(userId, definitionItemId) : null; + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).updateValue(this.room, userId, value); + boolean shouldEmit = changed || (hadBefore && Objects.equals(previousValue, value)); + + if (shouldEmit) { + boolean hasAfter = this.hasVariable(userId, definitionItemId); + Integer currentValue = hasAfter ? this.getCurrentValue(userId, definitionItemId) : null; + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.get(userId); + + if (assignments == null) { + return false; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + + if (assignment == null) { + return false; + } + + Integer normalizedValue = value; + if (Objects.equals(assignment.getValue(), normalizedValue)) { + this.emitVariableChangedEvents(userId, extra, definitionInfo, true, previousValue, true, assignment.getValue()); + return false; + } + + assignment.setValue(normalizedValue, Emulator.getIntUnixTimestamp()); + + WiredExtraUserVariable definition = (WiredExtraUserVariable) extra; + + if (definition.isPermanentAvailability()) { + this.upsertPersistentAssignment(userId, definitionItemId, assignment); + } + + if (definition.isSharedAvailability()) { + WiredVariableReferenceSupport.cacheSharedUserAssignment(this.room.getId(), definitionItemId, userId, assignment.getValue(), assignment.getCreatedAt(), assignment.getUpdatedAt()); + } + + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, true, assignment.getValue()); + this.broadcastSnapshot(); + return true; + } + + public int getCurrentValue(int userId, int definitionItemId) { + if (userId <= 0 || definitionItemId <= 0) { + return 0; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definitionItemId); + if (derivedDefinition != null) { + Integer baseValue = this.getRawValue(userId, derivedDefinition.getBaseDefinitionItemId()); + Integer derivedValue = WiredVariableLevelSystemSupport.getDerivedValue(derivedDefinition.getLevelSystem(), derivedDefinition.getSubvariableType(), baseValue); + return (derivedValue != null) ? derivedValue : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableReference) { + WiredVariableReferenceSupport.SharedUserAssignment assignment = WiredVariableReferenceSupport.getSharedUserAssignment((WiredExtraVariableReference) extra, userId); + return (assignment != null && assignment.getValue() != null) ? assignment.getValue() : 0; + } + + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getCurrentValue(this.room, userId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.get(userId); + + if (assignments == null) { + return 0; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + + if (assignment == null || assignment.getValue() == null) { + return 0; + } + + return assignment.getValue(); + } + + public int getCreatedAt(int userId, int definitionItemId) { + if (userId <= 0 || definitionItemId <= 0) { + return 0; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definitionItemId); + if (derivedDefinition != null) { + VariableAssignment assignment = this.getRawAssignment(userId, derivedDefinition.getBaseDefinitionItemId()); + return (assignment != null) ? assignment.getCreatedAt() : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableReference) { + WiredVariableReferenceSupport.SharedUserAssignment assignment = WiredVariableReferenceSupport.getSharedUserAssignment((WiredExtraVariableReference) extra, userId); + return assignment != null ? assignment.getCreatedAt() : 0; + } + + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getCreatedAt(this.room, userId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.get(userId); + + if (assignments == null) { + return 0; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + return assignment != null ? assignment.getCreatedAt() : 0; + } + + public int getUpdatedAt(int userId, int definitionItemId) { + if (userId <= 0 || definitionItemId <= 0) { + return 0; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definitionItemId); + if (derivedDefinition != null) { + VariableAssignment assignment = this.getRawAssignment(userId, derivedDefinition.getBaseDefinitionItemId()); + return (assignment != null) ? assignment.getUpdatedAt() : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableReference) { + WiredVariableReferenceSupport.SharedUserAssignment assignment = WiredVariableReferenceSupport.getSharedUserAssignment((WiredExtraVariableReference) extra, userId); + return assignment != null ? assignment.getUpdatedAt() : 0; + } + + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getUpdatedAt(this.room, userId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.get(userId); + + if (assignments == null) { + return 0; + } + + VariableAssignment assignment = assignments.get(definitionItemId); + return assignment != null ? assignment.getUpdatedAt() : 0; + } + + public boolean hasVariable(int userId, int definitionItemId) { + if (userId <= 0 || definitionItemId <= 0) { + return false; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definitionItemId); + if (derivedDefinition != null) { + return this.getRawAssignment(userId, derivedDefinition.getBaseDefinitionItemId()) != null; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableReference) { + return WiredVariableReferenceSupport.getSharedUserAssignment((WiredExtraVariableReference) extra, userId) != null; + } + + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).hasVariable(this.room, userId); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.get(userId); + + return assignments != null && assignments.containsKey(definitionItemId); + } + + public boolean removeVariable(int userId, int definitionItemId) { + if (userId <= 0 || definitionItemId <= 0) { + return false; + } + + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return false; + } + + boolean hadBefore = this.hasVariable(userId, definitionItemId); + Integer previousValue = (definitionInfo.hasValue() && hadBefore) ? this.getCurrentValue(userId, definitionItemId) : null; + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableReference) { + boolean changed = WiredVariableReferenceSupport.removeSharedUserVariable((WiredExtraVariableReference) extra, userId); + + if (changed) { + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, false, null); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).removeValue(this.room, userId); + + if (changed) { + boolean hasAfter = this.hasVariable(userId, definitionItemId); + Integer currentValue = (definitionInfo.hasValue() && hasAfter) ? this.getCurrentValue(userId, definitionItemId) : null; + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, hasAfter, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.get(userId); + + if (assignments == null) { + return false; + } + + if (assignments.remove(definitionItemId) == null) { + return false; + } + + if (assignments.isEmpty()) { + this.activeAssignmentsByUserId.remove(userId, assignments); + } + + this.deletePersistentAssignment(userId, definitionItemId); + + WiredExtraUserVariable definition = this.getDefinition(definitionItemId); + if (definition != null && definition.isSharedAvailability()) { + WiredVariableReferenceSupport.clearSharedUserAssignment(this.room.getId(), definitionItemId, userId); + } + + this.emitVariableChangedEvents(userId, extra, definitionInfo, hadBefore, previousValue, false, null); + this.broadcastSnapshot(); + + return true; + } + + public void clearAssignmentsForUser(int userId) { + if (userId <= 0) { + return; + } + + if (this.activeAssignmentsByUserId.remove(userId) != null) { + this.broadcastSnapshot(); + } + } + + public void removeDefinition(int definitionItemId) { + boolean changed = false; + + for (Map.Entry> entry : this.activeAssignmentsByUserId.entrySet()) { + ConcurrentHashMap assignments = entry.getValue(); + if (assignments.remove(definitionItemId) != null) { + changed = true; + } + + if (assignments.isEmpty()) { + this.activeAssignmentsByUserId.remove(entry.getKey(), assignments); + } + } + + this.deletePersistentAssignmentsForDefinition(definitionItemId); + WiredExtraUserVariable definition = this.getDefinition(definitionItemId); + if (definition != null && definition.isSharedAvailability()) { + WiredVariableReferenceSupport.clearSharedUserDefinition(this.room.getId(), definitionItemId); + } + + if (changed) { + this.broadcastSnapshot(); + return; + } + + this.broadcastSnapshot(); + } + + public void handleDefinitionUpdated(WiredExtraUserVariable definition) { + if (definition == null) { + return; + } + + if (!definition.isPermanentAvailability()) { + this.deletePersistentAssignmentsForDefinition(definition.getId()); + } else { + for (Map.Entry> entry : this.activeAssignmentsByUserId.entrySet()) { + VariableAssignment assignment = entry.getValue().get(definition.getId()); + + if (assignment == null) continue; + + this.upsertPersistentAssignment(entry.getKey(), definition.getId(), assignment); + } + } + + if (definition.isSharedAvailability()) { + for (Map.Entry> entry : this.activeAssignmentsByUserId.entrySet()) { + VariableAssignment assignment = entry.getValue().get(definition.getId()); + + if (assignment == null) { + continue; + } + + WiredVariableReferenceSupport.cacheSharedUserAssignment(this.room.getId(), definition.getId(), entry.getKey(), assignment.getValue(), assignment.getCreatedAt(), assignment.getUpdatedAt()); + } + } else { + WiredVariableReferenceSupport.clearSharedUserDefinition(this.room.getId(), definition.getId()); + } + + this.broadcastSnapshot(); + } + + public Snapshot createSnapshot() { + List definitions = new ArrayList<>(); + Map definitionsById = new LinkedHashMap<>(); + List derivedDefinitionIds = new ArrayList<>(); + + for (WiredVariableDefinitionInfo definition : this.getAllDefinitionInfos()) { + DefinitionEntry entry = new DefinitionEntry(definition.getItemId(), definition.getName(), definition.hasValue(), definition.getAvailability(), definition.isTextConnected(), definition.isReadOnly()); + definitions.add(entry); + definitionsById.put(entry.getItemId(), entry); + + if (WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definition.getItemId()) != null) { + derivedDefinitionIds.add(definition.getItemId()); + } + } + + List users = new ArrayList<>(); + List userReferences = this.getUserReferences(); + List userEchoes = this.getUserEchoes(); + THashSet userIds = new THashSet<>(); + userIds.addAll(this.activeAssignmentsByUserId.keySet()); + + for (Habbo habbo : this.room.getCurrentHabbos().values()) { + if (habbo != null) { + userIds.add(habbo.getHabboInfo().getId()); + } + } + + for (Integer userId : userIds) { + List assignments = new ArrayList<>(); + ConcurrentHashMap localAssignments = this.activeAssignmentsByUserId.get(userId); + + if (localAssignments != null) { + for (Map.Entry assignmentEntry : localAssignments.entrySet()) { + if (!definitionsById.containsKey(assignmentEntry.getKey())) { + continue; + } + + assignments.add(new AssignmentEntry( + assignmentEntry.getKey(), + assignmentEntry.getValue().getValue(), + assignmentEntry.getValue().getCreatedAt(), + assignmentEntry.getValue().getUpdatedAt() + )); + } + } + + for (WiredExtraVariableReference reference : userReferences) { + if (!definitionsById.containsKey(reference.getId())) { + continue; + } + + WiredVariableReferenceSupport.SharedUserAssignment assignment = WiredVariableReferenceSupport.getSharedUserAssignment(reference, userId); + if (assignment == null) { + continue; + } + + assignments.add(new AssignmentEntry(reference.getId(), assignment.getValue(), assignment.getCreatedAt(), assignment.getUpdatedAt())); + } + + for (WiredExtraVariableEcho echo : userEchoes) { + if (!definitionsById.containsKey(echo.getId()) || !echo.hasVariable(this.room, userId)) { + continue; + } + + assignments.add(new AssignmentEntry( + echo.getId(), + echo.getCurrentValue(this.room, userId), + echo.getCreatedAt(this.room, userId), + echo.getUpdatedAt(this.room, userId) + )); + } + + for (Integer derivedDefinitionId : derivedDefinitionIds) { + if (!this.hasVariable(userId, derivedDefinitionId)) { + continue; + } + + assignments.add(new AssignmentEntry( + derivedDefinitionId, + this.getCurrentValue(userId, derivedDefinitionId), + this.getCreatedAt(userId, derivedDefinitionId), + this.getUpdatedAt(userId, derivedDefinitionId) + )); + } + + assignments.sort(Comparator.comparingInt(AssignmentEntry::getVariableItemId)); + + if (!assignments.isEmpty()) { + users.add(new UserAssignmentsEntry(userId, assignments)); + } + } + + users.sort(Comparator.comparingInt(UserAssignmentsEntry::getUserId)); + + return new Snapshot(this.room.getId(), definitions, users); + } + + public void sendSnapshot(Habbo habbo) { + if (habbo == null || habbo.getClient() == null) { + return; + } + + if (!this.room.canInspectWired(habbo)) { + return; + } + + habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(this.createSnapshot(), this.room.getFurniVariableManager().createSnapshot(), this.room.getRoomVariableManager().createSnapshot())); + } + + public void broadcastSnapshot() { + Snapshot userSnapshot = this.createSnapshot(); + RoomFurniVariableManager.Snapshot furniSnapshot = this.room.getFurniVariableManager().createSnapshot(); + RoomVariableManager.Snapshot roomSnapshot = this.room.getRoomVariableManager().createSnapshot(); + + for (Habbo habbo : this.room.getCurrentHabbos().values()) { + if (habbo == null || habbo.getClient() == null) { + continue; + } + + if (!this.room.canInspectWired(habbo)) { + continue; + } + + habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(userSnapshot, furniSnapshot, roomSnapshot)); + } + } + + public Collection getDefinitions() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + THashSet extras = this.room.getRoomSpecialTypes().getExtras(); + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraUserVariable) { + WiredExtraUserVariable definition = (WiredExtraUserVariable) extra; + + if (!hasVisibleDefinitionName(definition.getVariableName())) { + continue; + } + + result.add(definition); + } + } + + result.sort(Comparator.comparing(WiredExtraUserVariable::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraUserVariable::getId)); + return result; + } + + public Collection getAllDefinitionInfos() { + List result = new ArrayList<>(); + List baseDefinitions = new ArrayList<>(); + + for (WiredExtraUserVariable definition : this.getDefinitions()) { + baseDefinitions.add(new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(this.room, definition), + false + )); + } + + for (WiredExtraVariableReference reference : this.getUserReferences()) { + baseDefinitions.add(new WiredVariableDefinitionInfo( + reference.getId(), + reference.getVariableName(), + reference.hasValue(), + reference.getAvailability(), + false, + reference.isReadOnly() + )); + } + + for (WiredExtraVariableEcho echo : this.getUserEchoes()) { + baseDefinitions.add(echo.createDefinitionInfo(this.room)); + } + + result.addAll(baseDefinitions); + + for (WiredVariableDefinitionInfo definition : baseDefinitions) { + result.addAll(WiredVariableLevelSystemSupport.getDerivedDefinitions(this.room, WiredVariableLevelSystemSupport.TARGET_USER, this.getDefinitionExtra(definition.getItemId()), definition)); + } + + result.sort(Comparator.comparing(WiredVariableDefinitionInfo::getName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredVariableDefinitionInfo::getItemId)); + return result; + } + + public boolean hasDefinition(int definitionItemId) { + return this.getDefinitionInfo(definitionItemId) != null; + } + + public WiredVariableDefinitionInfo getDefinitionInfo(int definitionItemId) { + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + + if (extra instanceof WiredExtraUserVariable) { + WiredExtraUserVariable definition = (WiredExtraUserVariable) extra; + + if (!hasVisibleDefinitionName(definition.getVariableName())) { + return null; + } + + return new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(this.room, definition), + false + ); + } + + if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isUserReference()) { + WiredExtraVariableReference reference = (WiredExtraVariableReference) extra; + + if (!hasVisibleDefinitionName(reference.getVariableName())) { + return null; + } + + return new WiredVariableDefinitionInfo(reference.getId(), reference.getVariableName(), reference.hasValue(), reference.getAvailability(), false, reference.isReadOnly()); + } + + if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isUserEcho()) { + WiredVariableDefinitionInfo info = ((WiredExtraVariableEcho) extra).createDefinitionInfo(this.room); + return (info != null && hasVisibleDefinitionName(info.getName())) ? info : null; + } + + return WiredVariableLevelSystemSupport.getDerivedDefinitionInfo(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definitionItemId); + } + + private WiredExtraUserVariable getDefinition(int definitionItemId) { + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + + if (!(extra instanceof WiredExtraUserVariable)) { + return null; + } + + return (WiredExtraUserVariable) extra; + } + + private InteractionWiredExtra getDefinitionExtra(int definitionItemId) { + if (this.room.getRoomSpecialTypes() == null || definitionItemId <= 0) { + return null; + } + + return this.room.getRoomSpecialTypes().getExtra(definitionItemId); + } + + private List getUserReferences() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { + if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isUserReference()) { + WiredExtraVariableReference reference = (WiredExtraVariableReference) extra; + + if (!hasVisibleDefinitionName(reference.getVariableName())) { + continue; + } + + result.add(reference); + } + } + + result.sort(Comparator.comparing(WiredExtraVariableReference::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraVariableReference::getId)); + return result; + } + + private List getUserEchoes() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { + if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isUserEcho()) { + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) extra; + + if (!hasVisibleDefinitionName(echo.getVariableName())) { + continue; + } + + result.add(echo); + } + } + + result.sort(Comparator.comparing(WiredExtraVariableEcho::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraVariableEcho::getId)); + return result; + } + + private static boolean hasVisibleDefinitionName(String name) { + return name != null && !name.trim().isEmpty(); + } + + private VariableAssignment getRawAssignment(int userId, int definitionItemId) { + if (userId <= 0 || definitionItemId <= 0) { + return null; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableReference) { + WiredVariableReferenceSupport.SharedUserAssignment assignment = WiredVariableReferenceSupport.getSharedUserAssignment((WiredExtraVariableReference) extra, userId); + return (assignment != null) ? new VariableAssignment(assignment.getValue(), assignment.getCreatedAt(), assignment.getUpdatedAt()) : null; + } + + if (extra instanceof WiredExtraVariableEcho) { + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) extra; + if (!echo.hasVariable(this.room, userId)) { + return null; + } + + return new VariableAssignment(echo.getCurrentValue(this.room, userId), echo.getCreatedAt(this.room, userId), echo.getUpdatedAt(this.room, userId)); + } + + ConcurrentHashMap assignments = this.activeAssignmentsByUserId.get(userId); + return (assignments != null) ? assignments.get(definitionItemId) : null; + } + + private Integer getRawValue(int userId, int definitionItemId) { + VariableAssignment assignment = this.getRawAssignment(userId, definitionItemId); + return (assignment != null) ? assignment.getValue() : null; + } + + private void emitVariableChangedEvents(int userId, InteractionWiredExtra definitionExtra, WiredVariableDefinitionInfo definitionInfo, boolean existedBefore, Integer previousValue, boolean existsAfter, Integer currentValue) { + if (definitionInfo == null) { + return; + } + + this.emitVariableChangedEvent(userId, definitionInfo.getItemId(), definitionInfo.hasValue(), existedBefore, previousValue, existsAfter, currentValue); + + for (WiredVariableDefinitionInfo derivedDefinition : WiredVariableLevelSystemSupport.getDerivedDefinitions(this.room, WiredVariableLevelSystemSupport.TARGET_USER, definitionExtra, definitionInfo)) { + WiredVariableLevelSystemSupport.DerivedDefinition resolvedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_USER, derivedDefinition.getItemId()); + + if (resolvedDefinition == null) { + continue; + } + + Integer derivedPreviousValue = existedBefore + ? WiredVariableLevelSystemSupport.getDerivedValue(resolvedDefinition.getLevelSystem(), resolvedDefinition.getSubvariableType(), previousValue) + : null; + Integer derivedCurrentValue = existsAfter + ? WiredVariableLevelSystemSupport.getDerivedValue(resolvedDefinition.getLevelSystem(), resolvedDefinition.getSubvariableType(), currentValue) + : null; + + this.emitVariableChangedEvent(userId, derivedDefinition.getItemId(), true, existedBefore, derivedPreviousValue, existsAfter, derivedCurrentValue); + } + } + + private void emitVariableChangedEvent(int userId, int definitionItemId, boolean hasValue, boolean existedBefore, Integer previousValue, boolean existsAfter, Integer currentValue) { + boolean created = !existedBefore && existsAfter; + boolean deleted = existedBefore && !existsAfter; + WiredEvent.VariableChangeKind changeKind = resolveVariableChangeKind(hasValue, existedBefore, previousValue, existsAfter, currentValue); + + if (!created && !deleted && changeKind == WiredEvent.VariableChangeKind.NONE) { + return; + } + + WiredManager.triggerUserVariableChanged(this.room, userId, definitionItemId, created, deleted, changeKind); + } + + private static WiredEvent.VariableChangeKind resolveVariableChangeKind(boolean hasValue, boolean existedBefore, Integer previousValue, boolean existsAfter, Integer currentValue) { + if (!hasValue || !existedBefore || !existsAfter) { + return WiredEvent.VariableChangeKind.NONE; + } + + if (Objects.equals(previousValue, currentValue)) { + return WiredEvent.VariableChangeKind.UNCHANGED; + } + + int previousNumericValue = (previousValue != null) ? previousValue : 0; + int currentNumericValue = (currentValue != null) ? currentValue : 0; + + return (currentNumericValue > previousNumericValue) + ? WiredEvent.VariableChangeKind.INCREASED + : WiredEvent.VariableChangeKind.DECREASED; + } + + private void upsertPersistentAssignment(int userId, int definitionItemId, VariableAssignment assignment) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO room_user_wired_variables (room_id, user_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, userId); + statement.setInt(3, definitionItemId); + + if (assignment == null || assignment.getValue() == null) { + statement.setNull(4, java.sql.Types.INTEGER); + } else { + statement.setInt(4, assignment.getValue()); + } + + int now = Emulator.getIntUnixTimestamp(); + statement.setInt(5, (assignment != null) ? assignment.getCreatedAt() : now); + statement.setInt(6, (assignment != null) ? assignment.getUpdatedAt() : now); + + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to store permanent wired user variable for room {}, user {}, item {}", this.room.getId(), userId, definitionItemId, e); + } + } + + private void deletePersistentAssignment(int userId, int definitionItemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND user_id = ? AND variable_item_id = ?")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, userId); + statement.setInt(3, definitionItemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete permanent wired user variable for room {}, user {}, item {}", this.room.getId(), userId, definitionItemId, e); + } + } + + private void deletePersistentAssignmentsForDefinition(int definitionItemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_user_wired_variables WHERE room_id = ? AND variable_item_id = ?")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, definitionItemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete permanent wired user variables for room {} and item {}", this.room.getId(), definitionItemId, e); + } + } + + public static class Snapshot { + private final int roomId; + private final List definitions; + private final List users; + + public Snapshot(int roomId, List definitions, List users) { + this.roomId = roomId; + this.definitions = definitions; + this.users = users; + } + + public int getRoomId() { + return roomId; + } + + public List getDefinitions() { + return definitions; + } + + public List getUsers() { + return users; + } + } + + public static class DefinitionEntry { + private final int itemId; + private final String name; + private final boolean hasValue; + private final int availability; + private final boolean textConnected; + private final boolean readOnly; + + public DefinitionEntry(int itemId, String name, boolean hasValue, int availability, boolean textConnected, boolean readOnly) { + this.itemId = itemId; + this.name = name; + this.hasValue = hasValue; + this.availability = availability; + this.textConnected = textConnected; + this.readOnly = readOnly; + } + + public int getItemId() { + return itemId; + } + + public String getName() { + return name; + } + + public boolean hasValue() { + return hasValue; + } + + public int getAvailability() { + return availability; + } + + public boolean isTextConnected() { + return this.textConnected; + } + + public boolean isReadOnly() { + return this.readOnly; + } + } + + public static class UserAssignmentsEntry { + private final int userId; + private final List assignments; + + public UserAssignmentsEntry(int userId, List assignments) { + this.userId = userId; + this.assignments = assignments; + } + + public int getUserId() { + return userId; + } + + public List getAssignments() { + return assignments; + } + } + + public static class AssignmentEntry { + private final int variableItemId; + private final Integer value; + private final int createdAt; + private final int updatedAt; + + public AssignmentEntry(int variableItemId, Integer value, int createdAt, int updatedAt) { + this.variableItemId = variableItemId; + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public int getVariableItemId() { + return variableItemId; + } + + public boolean hasValue() { + return value != null; + } + + public Integer getValue() { + return value; + } + + public int getCreatedAt() { + return createdAt; + } + + public int getUpdatedAt() { + return updatedAt; + } + } + + private static class VariableAssignment { + private Integer value; + private final int createdAt; + private int updatedAt; + + public VariableAssignment(Integer value, int createdAt, int updatedAt) { + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Integer getValue() { + return value; + } + + public void setValue(Integer value, int updatedAt) { + this.value = value; + this.updatedAt = updatedAt; + } + + public int getCreatedAt() { + return createdAt; + } + + public int getUpdatedAt() { + return updatedAt; + } + } + + private static int normalizeTimestamp(int value, int fallback) { + if (value > 0) return value; + if (fallback > 0) return fallback; + return Emulator.getIntUnixTimestamp(); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomVariableManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomVariableManager.java new file mode 100644 index 00000000..100b2d23 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomVariableManager.java @@ -0,0 +1,860 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRoomVariable; +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.WiredVariableReferenceSupport; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.wired.core.WiredEvent; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredVariableLevelSystemSupport; +import com.eu.habbo.habbohotel.wired.core.WiredVariableTextConnectorSupport; +import com.eu.habbo.messages.outgoing.wired.WiredUserVariablesDataComposer; +import gnu.trove.set.hash.THashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public class RoomVariableManager { + private static final Logger LOGGER = LoggerFactory.getLogger(RoomVariableManager.class); + + private final Room room; + private final ConcurrentHashMap activeAssignmentsByDefinitionId; + private volatile boolean persistentValuesLoaded; + + public RoomVariableManager(Room room) { + this.room = room; + this.activeAssignmentsByDefinitionId = new ConcurrentHashMap<>(); + this.persistentValuesLoaded = false; + } + + public void ensurePersistentValuesLoaded() { + if (this.persistentValuesLoaded) { + return; + } + + synchronized (this) { + if (this.persistentValuesLoaded) { + return; + } + + List staleDefinitionIds = new ArrayList<>(); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT variable_item_id, value, created_at, updated_at FROM room_wired_variables WHERE room_id = ?")) { + statement.setInt(1, this.room.getId()); + + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + int definitionItemId = set.getInt("variable_item_id"); + WiredExtraRoomVariable definition = this.getDefinition(definitionItemId); + + if (definition == null || !definition.isPermanentAvailability()) { + staleDefinitionIds.add(definitionItemId); + continue; + } + + int updatedAt = normalizeTimestamp(set.getInt("updated_at"), 0); + + this.activeAssignmentsByDefinitionId.put(definitionItemId, new VariableAssignment(set.getInt("value"), 0, updatedAt)); + } + } + } catch (SQLException e) { + LOGGER.error("Failed to restore wired room variables for room {}", this.room.getId(), e); + } + + for (Integer definitionItemId : staleDefinitionIds) { + this.deletePersistentAssignment(definitionItemId); + } + + this.persistentValuesLoaded = true; + } + } + + public int getCurrentValue(int definitionItemId) { + this.ensurePersistentValuesLoaded(); + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definitionItemId); + if (derivedDefinition != null) { + Integer baseValue = this.getRawValue(derivedDefinition.getBaseDefinitionItemId()); + Integer derivedValue = WiredVariableLevelSystemSupport.getDerivedValue(derivedDefinition.getLevelSystem(), derivedDefinition.getSubvariableType(), baseValue); + return (derivedValue != null) ? derivedValue : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getCurrentValue(this.room, this.room.getId()); + } + + if (extra instanceof WiredExtraVariableReference) { + WiredVariableReferenceSupport.SharedRoomAssignment assignment = WiredVariableReferenceSupport.getSharedRoomAssignment((WiredExtraVariableReference) extra); + return assignment != null ? assignment.getValue() : 0; + } + + VariableAssignment assignment = this.activeAssignmentsByDefinitionId.get(definitionItemId); + + return (assignment != null) ? assignment.getValue() : 0; + } + + public int getCreatedAt(int definitionItemId) { + this.ensurePersistentValuesLoaded(); + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definitionItemId); + if (derivedDefinition != null) { + VariableAssignment assignment = this.getRawAssignment(derivedDefinition.getBaseDefinitionItemId()); + return (assignment != null) ? assignment.getCreatedAt() : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getCreatedAt(this.room, this.room.getId()); + } + + if (extra instanceof WiredExtraVariableReference) { + return 0; + } + + VariableAssignment assignment = this.activeAssignmentsByDefinitionId.get(definitionItemId); + return (assignment != null) ? assignment.getCreatedAt() : 0; + } + + public int getUpdatedAt(int definitionItemId) { + this.ensurePersistentValuesLoaded(); + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definitionItemId); + if (derivedDefinition != null) { + VariableAssignment assignment = this.getRawAssignment(derivedDefinition.getBaseDefinitionItemId()); + return (assignment != null) ? assignment.getUpdatedAt() : 0; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).getUpdatedAt(this.room, this.room.getId()); + } + + if (extra instanceof WiredExtraVariableReference) { + WiredVariableReferenceSupport.SharedRoomAssignment assignment = WiredVariableReferenceSupport.getSharedRoomAssignment((WiredExtraVariableReference) extra); + return assignment != null ? assignment.getUpdatedAt() : 0; + } + + VariableAssignment assignment = this.activeAssignmentsByDefinitionId.get(definitionItemId); + return (assignment != null) ? assignment.getUpdatedAt() : 0; + } + + public boolean hasVariable(int definitionItemId) { + if (definitionItemId <= 0) { + return false; + } + + WiredVariableLevelSystemSupport.DerivedDefinition derivedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definitionItemId); + if (derivedDefinition != null) { + return this.getRawAssignment(derivedDefinition.getBaseDefinitionItemId()) != null; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + return ((WiredExtraVariableEcho) extra).hasVariable(this.room, this.room.getId()); + } + + return this.getDefinitionInfo(definitionItemId) != null; + } + + public boolean updateVariableValue(int definitionItemId, int value) { + this.ensurePersistentValuesLoaded(); + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return false; + } + + Integer previousValue = definitionInfo.hasValue() ? this.getCurrentValue(definitionItemId) : null; + + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).updateValue(this.room, this.room.getId(), value); + boolean shouldEmit = changed || (definitionInfo.hasValue() && previousValue != null && previousValue == value); + + if (shouldEmit) { + Integer currentValue = definitionInfo.hasValue() ? this.getCurrentValue(definitionItemId) : null; + this.emitVariableChangedEvents(extra, definitionInfo, previousValue, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + if (extra instanceof WiredExtraVariableReference) { + boolean changed = WiredVariableReferenceSupport.updateSharedRoomVariable((WiredExtraVariableReference) extra, value); + boolean shouldEmit = changed || (definitionInfo.hasValue() && previousValue != null && previousValue == value); + + if (shouldEmit) { + Integer currentValue = definitionInfo.hasValue() ? this.getCurrentValue(definitionItemId) : null; + this.emitVariableChangedEvents(extra, definitionInfo, previousValue, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + VariableAssignment assignment = this.activeAssignmentsByDefinitionId.get(definitionItemId); + + if (assignment == null) { + assignment = new VariableAssignment(value, 0, Emulator.getIntUnixTimestamp()); + this.activeAssignmentsByDefinitionId.put(definitionItemId, assignment); + } else if (assignment.getValue() == value) { + this.emitVariableChangedEvents(extra, definitionInfo, previousValue, assignment.getValue()); + return false; + } else { + assignment.setValue(value, Emulator.getIntUnixTimestamp()); + } + + WiredExtraRoomVariable definition = (WiredExtraRoomVariable) extra; + + if (definition.isPermanentAvailability()) { + this.upsertPersistentAssignment(definitionItemId, assignment); + } + + if (definition.isSharedAvailability()) { + WiredVariableReferenceSupport.cacheSharedRoomAssignment(this.room.getId(), definitionItemId, assignment.getValue(), assignment.getUpdatedAt()); + } else { + WiredVariableReferenceSupport.clearSharedRoomDefinition(this.room.getId(), definitionItemId); + } + + this.emitVariableChangedEvents(extra, definitionInfo, previousValue, assignment.getValue()); + this.broadcastSnapshot(); + return true; + } + + public boolean removeVariable(int definitionItemId) { + this.ensurePersistentValuesLoaded(); + + if (definitionItemId <= 0) { + return false; + } + + WiredVariableDefinitionInfo definitionInfo = this.getDefinitionInfo(definitionItemId); + if (definitionInfo == null || definitionInfo.isReadOnly()) { + return false; + } + + Integer previousValue = definitionInfo.hasValue() ? this.getCurrentValue(definitionItemId) : null; + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableEcho) { + boolean changed = ((WiredExtraVariableEcho) extra).removeValue(this.room, this.room.getId()); + + if (changed) { + Integer currentValue = definitionInfo.hasValue() ? this.getCurrentValue(definitionItemId) : null; + this.emitVariableChangedEvents(extra, definitionInfo, previousValue, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + if (extra instanceof WiredExtraVariableReference) { + boolean changed = WiredVariableReferenceSupport.removeSharedRoomVariable((WiredExtraVariableReference) extra); + + if (changed) { + Integer currentValue = definitionInfo.hasValue() ? this.getCurrentValue(definitionItemId) : null; + this.emitVariableChangedEvents(extra, definitionInfo, previousValue, currentValue); + } + + if (changed) { + this.broadcastSnapshot(); + } + + return changed; + } + + VariableAssignment removed = this.activeAssignmentsByDefinitionId.remove(definitionItemId); + this.deletePersistentAssignment(definitionItemId); + + WiredExtraRoomVariable definition = this.getDefinition(definitionItemId); + if (definition != null && definition.isSharedAvailability()) { + WiredVariableReferenceSupport.clearSharedRoomDefinition(this.room.getId(), definitionItemId); + } + + if (removed != null) { + Integer currentValue = definitionInfo.hasValue() ? this.getCurrentValue(definitionItemId) : null; + this.emitVariableChangedEvents(extra, definitionInfo, previousValue, currentValue); + this.broadcastSnapshot(); + return true; + } + + return false; + } + + public void clearTransientAssignments() { + this.ensurePersistentValuesLoaded(); + + boolean changed = false; + + for (Integer definitionItemId : new ArrayList<>(this.activeAssignmentsByDefinitionId.keySet())) { + WiredExtraRoomVariable definition = this.getDefinition(definitionItemId); + + if (definition != null && definition.isPermanentAvailability()) { + continue; + } + + if (this.activeAssignmentsByDefinitionId.remove(definitionItemId) != null) { + changed = true; + } + } + + if (changed) { + this.broadcastSnapshot(); + } + } + + public void removeDefinition(int definitionItemId) { + this.ensurePersistentValuesLoaded(); + this.activeAssignmentsByDefinitionId.remove(definitionItemId); + this.deletePersistentAssignment(definitionItemId); + WiredExtraRoomVariable definition = this.getDefinition(definitionItemId); + if (definition != null && definition.isSharedAvailability()) { + WiredVariableReferenceSupport.clearSharedRoomDefinition(this.room.getId(), definitionItemId); + } + this.broadcastSnapshot(); + } + + public void handleDefinitionUpdated(WiredExtraRoomVariable definition) { + if (definition == null) { + return; + } + + this.ensurePersistentValuesLoaded(); + + if (!definition.isPermanentAvailability()) { + this.deletePersistentAssignment(definition.getId()); + } else { + VariableAssignment assignment = this.activeAssignmentsByDefinitionId.get(definition.getId()); + + if (assignment == null) { + return; + } + + this.upsertPersistentAssignment(definition.getId(), assignment); + } + + if (definition.isSharedAvailability()) { + VariableAssignment assignment = this.activeAssignmentsByDefinitionId.get(definition.getId()); + + if (assignment != null) { + WiredVariableReferenceSupport.cacheSharedRoomAssignment(this.room.getId(), definition.getId(), assignment.getValue(), assignment.getUpdatedAt()); + } + } else { + WiredVariableReferenceSupport.clearSharedRoomDefinition(this.room.getId(), definition.getId()); + } + + this.broadcastSnapshot(); + } + + public Snapshot createSnapshot() { + this.ensurePersistentValuesLoaded(); + + List definitions = new ArrayList<>(); + List assignments = new ArrayList<>(); + List derivedDefinitionIds = new ArrayList<>(); + List roomEchoes = this.getRoomEchoes(); + + for (WiredVariableDefinitionInfo definition : this.getAllDefinitionInfos()) { + definitions.add(new DefinitionEntry(definition.getItemId(), definition.getName(), definition.hasValue(), definition.getAvailability(), definition.isTextConnected(), definition.isReadOnly())); + + if (WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definition.getItemId()) != null) { + derivedDefinitionIds.add(definition.getItemId()); + } + + if (this.isReferenceDefinition(definition.getItemId())) { + WiredExtraVariableReference reference = (WiredExtraVariableReference) this.getDefinitionExtra(definition.getItemId()); + WiredVariableReferenceSupport.SharedRoomAssignment assignment = WiredVariableReferenceSupport.getSharedRoomAssignment(reference); + assignments.add(new AssignmentEntry(definition.getItemId(), (assignment != null) ? assignment.getValue() : 0, 0, (assignment != null) ? assignment.getUpdatedAt() : 0)); + continue; + } + + if (derivedDefinitionIds.contains(definition.getItemId())) { + assignments.add(new AssignmentEntry( + definition.getItemId(), + this.getCurrentValue(definition.getItemId()), + this.getCreatedAt(definition.getItemId()), + this.getUpdatedAt(definition.getItemId()) + )); + continue; + } + + if (roomEchoes.stream().anyMatch(echo -> echo.getId() == definition.getItemId())) { + assignments.add(new AssignmentEntry( + definition.getItemId(), + this.getCurrentValue(definition.getItemId()), + this.getCreatedAt(definition.getItemId()), + this.getUpdatedAt(definition.getItemId()) + )); + continue; + } + + VariableAssignment assignment = this.activeAssignmentsByDefinitionId.get(definition.getItemId()); + assignments.add(new AssignmentEntry(definition.getItemId(), (assignment != null) ? assignment.getValue() : 0, 0, (assignment != null) ? assignment.getUpdatedAt() : 0)); + } + + assignments.sort(Comparator.comparingInt(AssignmentEntry::getVariableItemId)); + + return new Snapshot(this.room.getId(), definitions, assignments); + } + + public void sendSnapshot(Habbo habbo) { + if (habbo == null || habbo.getClient() == null || !this.room.canInspectWired(habbo)) { + return; + } + + habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(this.room.getUserVariableManager().createSnapshot(), this.room.getFurniVariableManager().createSnapshot(), this.createSnapshot())); + } + + public void broadcastSnapshot() { + RoomUserVariableManager.Snapshot userSnapshot = this.room.getUserVariableManager().createSnapshot(); + RoomFurniVariableManager.Snapshot furniSnapshot = this.room.getFurniVariableManager().createSnapshot(); + Snapshot roomSnapshot = this.createSnapshot(); + + for (Habbo habbo : this.room.getCurrentHabbos().values()) { + if (habbo == null || habbo.getClient() == null || !this.room.canInspectWired(habbo)) { + continue; + } + + habbo.getClient().sendResponse(new WiredUserVariablesDataComposer(userSnapshot, furniSnapshot, roomSnapshot)); + } + } + + public Collection getDefinitions() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + THashSet extras = this.room.getRoomSpecialTypes().getExtras(); + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraRoomVariable) { + WiredExtraRoomVariable definition = (WiredExtraRoomVariable) extra; + + if (!hasVisibleDefinitionName(definition.getVariableName())) { + continue; + } + + result.add(definition); + } + } + + result.sort(Comparator.comparing(WiredExtraRoomVariable::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraRoomVariable::getId)); + return result; + } + + public Collection getAllDefinitionInfos() { + List result = new ArrayList<>(); + List baseDefinitions = new ArrayList<>(); + + for (WiredExtraRoomVariable definition : this.getDefinitions()) { + baseDefinitions.add(new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(this.room, definition), + false + )); + } + + for (WiredExtraVariableReference reference : this.getRoomReferences()) { + baseDefinitions.add(new WiredVariableDefinitionInfo(reference.getId(), reference.getVariableName(), reference.hasValue(), reference.getAvailability(), false, reference.isReadOnly())); + } + + for (WiredExtraVariableEcho echo : this.getRoomEchoes()) { + baseDefinitions.add(echo.createDefinitionInfo(this.room)); + } + + result.addAll(baseDefinitions); + + for (WiredVariableDefinitionInfo definition : baseDefinitions) { + result.addAll(WiredVariableLevelSystemSupport.getDerivedDefinitions(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, this.getDefinitionExtra(definition.getItemId()), definition)); + } + + result.sort(Comparator.comparing(WiredVariableDefinitionInfo::getName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredVariableDefinitionInfo::getItemId)); + return result; + } + + public WiredVariableDefinitionInfo getDefinitionInfo(int definitionItemId) { + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + + if (extra instanceof WiredExtraRoomVariable) { + WiredExtraRoomVariable definition = (WiredExtraRoomVariable) extra; + + if (!hasVisibleDefinitionName(definition.getVariableName())) { + return null; + } + + return new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(this.room, definition), + false + ); + } + + if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isRoomReference()) { + WiredExtraVariableReference reference = (WiredExtraVariableReference) extra; + + if (!hasVisibleDefinitionName(reference.getVariableName())) { + return null; + } + + return new WiredVariableDefinitionInfo(reference.getId(), reference.getVariableName(), reference.hasValue(), reference.getAvailability(), false, reference.isReadOnly()); + } + + if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isRoomEcho()) { + WiredVariableDefinitionInfo info = ((WiredExtraVariableEcho) extra).createDefinitionInfo(this.room); + return (info != null && hasVisibleDefinitionName(info.getName())) ? info : null; + } + + return WiredVariableLevelSystemSupport.getDerivedDefinitionInfo(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definitionItemId); + } + + private WiredExtraRoomVariable getDefinition(int definitionItemId) { + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + + if (!(extra instanceof WiredExtraRoomVariable)) { + return null; + } + + return (WiredExtraRoomVariable) extra; + } + + private InteractionWiredExtra getDefinitionExtra(int definitionItemId) { + if (this.room.getRoomSpecialTypes() == null || definitionItemId <= 0) { + return null; + } + + return this.room.getRoomSpecialTypes().getExtra(definitionItemId); + } + + private boolean isReferenceDefinition(int definitionItemId) { + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + return extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isRoomReference(); + } + + private List getRoomReferences() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { + if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isRoomReference()) { + WiredExtraVariableReference reference = (WiredExtraVariableReference) extra; + + if (!hasVisibleDefinitionName(reference.getVariableName())) { + continue; + } + + result.add(reference); + } + } + + result.sort(Comparator.comparing(WiredExtraVariableReference::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraVariableReference::getId)); + return result; + } + + private List getRoomEchoes() { + if (this.room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + + for (InteractionWiredExtra extra : this.room.getRoomSpecialTypes().getExtras()) { + if (extra instanceof WiredExtraVariableEcho && ((WiredExtraVariableEcho) extra).isRoomEcho()) { + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) extra; + + if (!hasVisibleDefinitionName(echo.getVariableName())) { + continue; + } + + result.add(echo); + } + } + + result.sort(Comparator.comparing(WiredExtraVariableEcho::getVariableName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredExtraVariableEcho::getId)); + return result; + } + + private VariableAssignment getRawAssignment(int definitionItemId) { + if (definitionItemId <= 0) { + return null; + } + + InteractionWiredExtra extra = this.getDefinitionExtra(definitionItemId); + if (extra instanceof WiredExtraVariableReference) { + WiredVariableReferenceSupport.SharedRoomAssignment assignment = WiredVariableReferenceSupport.getSharedRoomAssignment((WiredExtraVariableReference) extra); + return (assignment != null) ? new VariableAssignment(assignment.getValue(), 0, assignment.getUpdatedAt()) : null; + } + + if (extra instanceof WiredExtraVariableEcho) { + WiredExtraVariableEcho echo = (WiredExtraVariableEcho) extra; + if (!echo.hasVariable(this.room, this.room.getId())) { + return null; + } + + return new VariableAssignment(echo.getCurrentValue(this.room, this.room.getId()), echo.getCreatedAt(this.room, this.room.getId()), echo.getUpdatedAt(this.room, this.room.getId())); + } + + return this.activeAssignmentsByDefinitionId.get(definitionItemId); + } + + private Integer getRawValue(int definitionItemId) { + VariableAssignment assignment = this.getRawAssignment(definitionItemId); + return (assignment != null) ? assignment.getValue() : null; + } + + private void emitVariableChangedEvents(InteractionWiredExtra definitionExtra, WiredVariableDefinitionInfo definitionInfo, Integer previousValue, Integer currentValue) { + if (definitionInfo == null) { + return; + } + + this.emitVariableChangedEvent(definitionInfo.getItemId(), definitionInfo.hasValue(), previousValue, currentValue); + + for (WiredVariableDefinitionInfo derivedDefinition : WiredVariableLevelSystemSupport.getDerivedDefinitions(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, definitionExtra, definitionInfo)) { + WiredVariableLevelSystemSupport.DerivedDefinition resolvedDefinition = WiredVariableLevelSystemSupport.resolveDerivedDefinition(this.room, WiredVariableLevelSystemSupport.TARGET_ROOM, derivedDefinition.getItemId()); + + if (resolvedDefinition == null) { + continue; + } + + Integer derivedPreviousValue = WiredVariableLevelSystemSupport.getDerivedValue(resolvedDefinition.getLevelSystem(), resolvedDefinition.getSubvariableType(), previousValue); + Integer derivedCurrentValue = WiredVariableLevelSystemSupport.getDerivedValue(resolvedDefinition.getLevelSystem(), resolvedDefinition.getSubvariableType(), currentValue); + + this.emitVariableChangedEvent(derivedDefinition.getItemId(), true, derivedPreviousValue, derivedCurrentValue); + } + } + + private void emitVariableChangedEvent(int definitionItemId, boolean hasValue, Integer previousValue, Integer currentValue) { + WiredEvent.VariableChangeKind changeKind = resolveVariableChangeKind(hasValue, previousValue, currentValue); + + if (changeKind == WiredEvent.VariableChangeKind.NONE) { + return; + } + + WiredManager.triggerRoomVariableChanged(this.room, definitionItemId, changeKind); + } + + private static WiredEvent.VariableChangeKind resolveVariableChangeKind(boolean hasValue, Integer previousValue, Integer currentValue) { + if (!hasValue) { + return WiredEvent.VariableChangeKind.NONE; + } + + if (Objects.equals(previousValue, currentValue)) { + return WiredEvent.VariableChangeKind.UNCHANGED; + } + + int previousNumericValue = (previousValue != null) ? previousValue : 0; + int currentNumericValue = (currentValue != null) ? currentValue : 0; + + return (currentNumericValue > previousNumericValue) + ? WiredEvent.VariableChangeKind.INCREASED + : WiredEvent.VariableChangeKind.DECREASED; + } + + private void upsertPersistentAssignment(int definitionItemId, VariableAssignment assignment) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO room_wired_variables (room_id, variable_item_id, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, definitionItemId); + statement.setInt(3, (assignment != null) ? assignment.getValue() : 0); + + int now = Emulator.getIntUnixTimestamp(); + statement.setInt(4, 0); + statement.setInt(5, (assignment != null) ? assignment.getUpdatedAt() : now); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to store permanent wired room variable for room {} and item {}", this.room.getId(), definitionItemId, e); + } + } + + private void deletePersistentAssignment(int definitionItemId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM room_wired_variables WHERE room_id = ? AND variable_item_id = ?")) { + statement.setInt(1, this.room.getId()); + statement.setInt(2, definitionItemId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to delete permanent wired room variable for room {} and item {}", this.room.getId(), definitionItemId, e); + } + } + + private static int normalizeTimestamp(int value, int fallback) { + if (value > 0) { + return value; + } + + if (fallback > 0) { + return fallback; + } + return 0; + } + + private static boolean hasVisibleDefinitionName(String name) { + return name != null && !name.trim().isEmpty(); + } + + public static class Snapshot { + private final int roomId; + private final List definitions; + private final List assignments; + + public Snapshot(int roomId, List definitions, List assignments) { + this.roomId = roomId; + this.definitions = definitions; + this.assignments = assignments; + } + + public int getRoomId() { + return this.roomId; + } + + public List getDefinitions() { + return this.definitions; + } + + public List getAssignments() { + return this.assignments; + } + } + + public static class DefinitionEntry { + private final int itemId; + private final String name; + private final boolean hasValue; + private final int availability; + private final boolean textConnected; + private final boolean readOnly; + + public DefinitionEntry(int itemId, String name, boolean hasValue, int availability, boolean textConnected, boolean readOnly) { + this.itemId = itemId; + this.name = name; + this.hasValue = hasValue; + this.availability = availability; + this.textConnected = textConnected; + this.readOnly = readOnly; + } + + public int getItemId() { + return this.itemId; + } + + public String getName() { + return this.name; + } + + public boolean hasValue() { + return this.hasValue; + } + + public int getAvailability() { + return this.availability; + } + + public boolean isTextConnected() { + return this.textConnected; + } + + public boolean isReadOnly() { + return this.readOnly; + } + } + + public static class AssignmentEntry { + private final int variableItemId; + private final Integer value; + private final int createdAt; + private final int updatedAt; + + public AssignmentEntry(int variableItemId, Integer value, int createdAt, int updatedAt) { + this.variableItemId = variableItemId; + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public int getVariableItemId() { + return this.variableItemId; + } + + public Integer getValue() { + return this.value; + } + + public boolean hasValue() { + return this.value != null; + } + + public int getCreatedAt() { + return this.createdAt; + } + + public int getUpdatedAt() { + return this.updatedAt; + } + } + + private static class VariableAssignment { + private int value; + private final int createdAt; + private int updatedAt; + + public VariableAssignment(int value, int createdAt, int updatedAt) { + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public int getValue() { + return this.value; + } + + public void setValue(int value, int updatedAt) { + this.value = value; + this.updatedAt = updatedAt; + } + + public int getCreatedAt() { + return this.createdAt; + } + + public int getUpdatedAt() { + return this.updatedAt; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomWiredDisableSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomWiredDisableSupport.java new file mode 100644 index 00000000..9aa33f59 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomWiredDisableSupport.java @@ -0,0 +1,47 @@ +package com.eu.habbo.habbohotel.rooms; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredDisableControl; +import com.eu.habbo.habbohotel.users.HabboItem; + +public final class RoomWiredDisableSupport { + private static final String CONTROLLER_INTERACTION = "wf_conf_wired_disable"; + + private RoomWiredDisableSupport() { + } + + public static boolean isWiredDisabled(Room room) { + if (room == null) { + return false; + } + + for (HabboItem item : room.getFloorItems()) { + if (isActiveController(item)) { + return true; + } + } + + return false; + } + + public static boolean isActiveController(HabboItem item) { + return isControllerItem(item) && "1".equals(item.getExtradata()); + } + + public static boolean isControllerItem(HabboItem item) { + if (item == null || item.getBaseItem() == null) { + return false; + } + + if (item instanceof InteractionWiredDisableControl) { + return true; + } + + if (item.getBaseItem().getInteractionType() == null) { + return false; + } + + String interactionName = item.getBaseItem().getInteractionType().getName(); + + return interactionName != null && interactionName.equalsIgnoreCase(CONTROLLER_INTERACTION); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/WiredVariableDefinitionInfo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/WiredVariableDefinitionInfo.java new file mode 100644 index 00000000..26a4a79d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/WiredVariableDefinitionInfo.java @@ -0,0 +1,43 @@ +package com.eu.habbo.habbohotel.rooms; + +public class WiredVariableDefinitionInfo { + private final int itemId; + private final String name; + private final boolean hasValue; + private final int availability; + private final boolean textConnected; + private final boolean readOnly; + + public WiredVariableDefinitionInfo(int itemId, String name, boolean hasValue, int availability, boolean textConnected, boolean readOnly) { + this.itemId = itemId; + this.name = name; + this.hasValue = hasValue; + this.availability = availability; + this.textConnected = textConnected; + this.readOnly = readOnly; + } + + public int getItemId() { + return this.itemId; + } + + public String getName() { + return this.name; + } + + public boolean hasValue() { + return this.hasValue; + } + + public int getAvailability() { + return this.availability; + } + + public boolean isTextConnected() { + return this.textConnected; + } + + public boolean isReadOnly() { + return this.readOnly; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/Habbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/Habbo.java index 0bc6a604..61f8075e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/Habbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/Habbo.java @@ -259,7 +259,7 @@ public class Habbo implements Runnable { this.getHabboInfo().addCredits(event.credits); - if (this.client != null) this.client.sendResponse(new UserCreditsComposer(this.client.getHabbo())); + if (this.client != null) this.client.sendResponse(new UserCreditsComposer(this)); } @@ -273,7 +273,7 @@ public class Habbo implements Runnable { return; this.getHabboInfo().addPixels(event.points); - if (this.client != null) this.client.sendResponse(new UserCurrencyComposer(this.client.getHabbo())); + if (this.client != null) this.client.sendResponse(new UserCurrencyComposer(this)); } @@ -292,7 +292,7 @@ public class Habbo implements Runnable { this.getHabboInfo().addCurrencyAmount(event.type, event.points); if (this.client != null) - this.client.sendResponse(new UserPointsComposer(this.client.getHabbo().getHabboInfo().getCurrencyAmount(type), event.points, event.type)); + this.client.sendResponse(new UserPointsComposer(this.getHabboInfo().getCurrencyAmount(type), event.points, event.type)); } @@ -303,7 +303,7 @@ public class Habbo implements Runnable { public void whisper(String message, RoomChatMessageBubbles bubble) { if (this.getRoomUnit().isInRoom()) { - this.client.sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(message, this.client.getHabbo().getRoomUnit(), bubble))); + this.client.sendResponse(new RoomUserWhisperComposer(new RoomChatMessage(message, this.getRoomUnit(), bubble))); } } @@ -315,7 +315,7 @@ public class Habbo implements Runnable { public void talk(String message, RoomChatMessageBubbles bubble) { if (this.getRoomUnit().isInRoom()) { - this.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserTalkComposer(new RoomChatMessage(message, this.client.getHabbo().getRoomUnit(), bubble)).compose()); + this.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserTalkComposer(new RoomChatMessage(message, this.getRoomUnit(), bubble)).compose()); } } @@ -327,7 +327,7 @@ public class Habbo implements Runnable { public void shout(String message, RoomChatMessageBubbles bubble) { if (this.getRoomUnit().isInRoom()) { - this.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserShoutComposer(new RoomChatMessage(message, this.client.getHabbo().getRoomUnit(), bubble)).compose()); + this.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserShoutComposer(new RoomChatMessage(message, this.getRoomUnit(), bubble)).compose()); } } @@ -408,10 +408,14 @@ public class Habbo implements Runnable { public boolean addBadge(String code) { + return this.addBadge(code, ""); + } + + public boolean addBadge(String code, String senderName) { if (!this.habboInventory.getBadgesComponent().hasBadge(code)) { HabboBadge badge = BadgesComponent.createBadge(code, this); this.habboInventory.getBadgesComponent().addBadge(badge); - this.client.sendResponse(new AddUserBadgeComposer(badge)); + this.client.sendResponse(new AddUserBadgeComposer(badge, senderName)); this.client.sendResponse(new AddHabboItemComposer(badge.getId(), AddHabboItemComposer.AddHabboItemCategory.BADGE)); THashMap keys = new THashMap<>(); @@ -446,7 +450,7 @@ public class Habbo implements Runnable { this.client.sendResponse(new FloodCounterComposer(remaining)); this.client.sendResponse(new MutedWhisperComposer(remaining)); - Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + Room room = this.getHabboInfo().getCurrentRoom(); if (room != null && !isFlood) { room.sendComposer(new RoomUserIgnoredComposer(this, RoomUserIgnoredComposer.MUTED).compose()); } @@ -456,7 +460,7 @@ public class Habbo implements Runnable { public void unMute() { this.habboStats.unMute(); this.client.sendResponse(new FloodCounterComposer(3)); - Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + Room room = this.getHabboInfo().getCurrentRoom(); if (room != null) { room.sendComposer(new RoomUserIgnoredComposer(this, RoomUserIgnoredComposer.UNIGNORED).compose()); } @@ -493,18 +497,18 @@ public class Habbo implements Runnable { public void respect(Habbo target) { - if (target != null && target != this.client.getHabbo()) { + if (target != null && target != this) { target.getHabboStats().respectPointsReceived++; - this.client.getHabbo().getHabboStats().respectPointsGiven++; - this.client.getHabbo().getHabboStats().respectPointsToGive--; - this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserRespectComposer(target).compose()); - this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserActionComposer(this.client.getHabbo().getRoomUnit(), RoomUserAction.THUMB_UP).compose()); + this.getHabboStats().respectPointsGiven++; + this.getHabboStats().respectPointsToGive--; + this.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserRespectComposer(target).compose()); + this.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserActionComposer(this.getRoomUnit(), RoomUserAction.THUMB_UP).compose()); - AchievementManager.progressAchievement(this.client.getHabbo(), Emulator.getGameEnvironment().getAchievementManager().getAchievement("RespectGiven")); + AchievementManager.progressAchievement(this, Emulator.getGameEnvironment().getAchievementManager().getAchievement("RespectGiven")); AchievementManager.progressAchievement(target, Emulator.getGameEnvironment().getAchievementManager().getAchievement("RespectEarned")); - this.client.getHabbo().getHabboInfo().getCurrentRoom().unIdle(this.client.getHabbo()); - this.client.getHabbo().getHabboInfo().getCurrentRoom().dance(this.client.getHabbo().getRoomUnit(), DanceType.NONE); + this.getHabboInfo().getCurrentRoom().unIdle(this); + this.getHabboInfo().getCurrentRoom().dance(this.getRoomUnit(), DanceType.NONE); } } 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 c0524b9e..e53aee11 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 @@ -1,6 +1,7 @@ package com.eu.habbo.habbohotel.users; import com.eu.habbo.Emulator; +import com.eu.habbo.database.SqlQueries; import com.eu.habbo.habbohotel.catalog.CatalogItem; import com.eu.habbo.habbohotel.games.Game; import com.eu.habbo.habbohotel.games.GamePlayer; @@ -14,7 +15,6 @@ import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer; import gnu.trove.map.hash.TIntIntHashMap; -import gnu.trove.procedure.TIntIntProcedure; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,6 +47,8 @@ public class HabboInfo implements Runnable { private int InfostandOverlay; private int loadingRoom; private Room currentRoom; + private String roomEntryMethod = "door"; + private int roomEntryTeleportId = 0; private int roomQueueId; private RideablePet riding; private Class currentGame; @@ -102,53 +104,47 @@ public class HabboInfo implements Runnable { private void loadCurrencies() { this.currencies = new TIntIntHashMap(); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users_currency WHERE user_id = ?")) { - statement.setInt(1, this.id); - try (ResultSet set = statement.executeQuery()) { - while (set.next()) { - this.currencies.put(set.getInt("type"), set.getInt("amount")); - } - } - } catch (SQLException e) { + try { + SqlQueries.forEach( + "SELECT * FROM users_currency WHERE user_id = ?", + rs -> this.currencies.put(rs.getInt("type"), rs.getInt("amount")), + this.id); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); } } private void saveCurrencies() { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_currency (user_id, type, amount) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE amount = ?")) { - this.currencies.forEachEntry(new TIntIntProcedure() { - @Override - public boolean execute(int a, int b) { - try { - statement.setInt(1, HabboInfo.this.getId()); - statement.setInt(2, a); - statement.setInt(3, b); - statement.setInt(4, b); - statement.addBatch(); - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } - return true; - } - }); - statement.executeBatch(); - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); + List entries = new ArrayList<>(this.currencies.size()); + this.currencies.forEachEntry((type, amount) -> { + entries.add(new int[]{type, amount}); + return true; + }); + + try { + SqlQueries.batchUpdate( + "INSERT INTO users_currency (user_id, type, amount) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE amount = ?", + entries, + (ps, e) -> { + ps.setInt(1, this.id); + ps.setInt(2, e[0]); + ps.setInt(3, e[1]); + ps.setInt(4, e[1]); + }); + } catch (SqlQueries.DataAccessException ex) { + LOGGER.error("Caught SQL exception", ex); } } private void loadSavedSearches() { - this.savedSearches = new ArrayList<>(); - - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users_saved_searches WHERE user_id = ?")) { - statement.setInt(1, this.id); - try (ResultSet set = statement.executeQuery()) { - while (set.next()) { - this.savedSearches.add(new NavigatorSavedSearch(set.getString("search_code"), set.getString("filter"), set.getInt("id"))); - } - } - } catch (SQLException e) { + try { + this.savedSearches = SqlQueries.query( + "SELECT * FROM users_saved_searches WHERE user_id = ?", + rs -> new NavigatorSavedSearch(rs.getString("search_code"), rs.getString("filter"), rs.getInt("id")), + this.id); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); + this.savedSearches = new ArrayList<>(); } } @@ -180,26 +176,22 @@ public class HabboInfo implements Runnable { public void deleteSavedSearch(NavigatorSavedSearch search) { this.savedSearches.remove(search); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("DELETE FROM users_saved_searches WHERE id = ?")) { - statement.setInt(1, search.getId()); - statement.execute(); - } catch (SQLException e) { + try { + SqlQueries.update("DELETE FROM users_saved_searches WHERE id = ?", search.getId()); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); } } private void loadMessengerCategories() { - this.messengerCategories = new ArrayList<>(); - - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM messenger_categories WHERE user_id = ?")) { - statement.setInt(1, this.id); - try (ResultSet set = statement.executeQuery()) { - while (set.next()) { - this.messengerCategories.add(new MessengerCategory(set.getString("name"), set.getInt("user_id"), set.getInt("id"))); - } - } - } catch (SQLException e) { + try { + this.messengerCategories = SqlQueries.query( + "SELECT * FROM messenger_categories WHERE user_id = ?", + rs -> new MessengerCategory(rs.getString("name"), rs.getInt("user_id"), rs.getInt("id")), + this.id); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); + this.messengerCategories = new ArrayList<>(); } } @@ -230,10 +222,9 @@ public class HabboInfo implements Runnable { public void deleteMessengerCategory(MessengerCategory category) { this.messengerCategories.remove(category); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("DELETE FROM messenger_categories WHERE id = ?")) { - statement.setInt(1, category.getId()); - statement.execute(); - } catch (SQLException e) { + try { + SqlQueries.update("DELETE FROM messenger_categories WHERE id = ?", category.getId()); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); } } @@ -387,12 +378,10 @@ public class HabboInfo implements Runnable { public void setPixels(int pixels) { this.setCurrencyAmount(0, pixels); - this.run(); } public void addPixels(int pixels) { this.addCurrencyAmount(0, pixels); - this.run(); } public int getLastOnline() { @@ -435,6 +424,22 @@ public class HabboInfo implements Runnable { this.currentRoom = room; } + public String getRoomEntryMethod() { + return this.roomEntryMethod; + } + + public void setRoomEntryMethod(String roomEntryMethod) { + this.roomEntryMethod = roomEntryMethod; + } + + public int getRoomEntryTeleportId() { + return this.roomEntryTeleportId; + } + + public void setRoomEntryTeleportId(int roomEntryTeleportId) { + this.roomEntryTeleportId = roomEntryTeleportId; + } + public int getRoomQueueId() { return this.roomQueueId; } @@ -570,25 +575,26 @@ public class HabboInfo implements Runnable { public void run() { this.saveCurrencies(); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("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 = ?")) { - statement.setString(1, this.motto); - statement.setString(2, this.online ? "1" : "0"); - statement.setString(3, this.look); - statement.setString(4, this.gender.name()); - statement.setInt(5, this.credits); - statement.setInt(7, this.lastOnline); - statement.setInt(6, Emulator.getIntUnixTimestamp()); - statement.setInt(8, this.homeRoom); - statement.setString(9, this.ipLogin); - statement.setInt(10, this.rank != null ? this.rank.getId() : 1); - statement.setString(11, this.machineID); - statement.setString(12, this.username); - statement.setInt(13, this.InfostandBg); - statement.setInt(14, this.InfostandStand); - statement.setInt(15, this.InfostandOverlay); - statement.setInt(16, this.id); - statement.executeUpdate(); - } catch (SQLException e) { + 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 = ?", + this.motto, + this.online ? "1" : "0", + this.look, + this.gender.name(), + this.credits, + Emulator.getIntUnixTimestamp(), + this.lastOnline, + this.homeRoom, + this.ipLogin, + this.rank != null ? this.rank.getId() : 1, + this.machineID, + this.username, + this.InfostandBg, + this.InfostandStand, + this.InfostandOverlay, + this.id); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInventory.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInventory.java index 7eaf048d..6fdba07d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInventory.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInventory.java @@ -22,6 +22,7 @@ public class HabboInventory { private EffectsComponent effectsComponent; private ItemsComponent itemsComponent; private PetsComponent petsComponent; + private PrefixesComponent prefixesComponent; public HabboInventory(Habbo habbo) { this.habbo = habbo; @@ -61,6 +62,12 @@ public class HabboInventory { LOGGER.error("Caught exception", e); } + try { + this.prefixesComponent = new PrefixesComponent(this.habbo); + } catch (Exception e) { + LOGGER.error("Caught exception", e); + } + this.items = MarketPlace.getOwnOffers(this.habbo); } @@ -112,6 +119,14 @@ public class HabboInventory { this.petsComponent = petsComponent; } + public PrefixesComponent getPrefixesComponent() { + return this.prefixesComponent; + } + + public void setPrefixesComponent(PrefixesComponent prefixesComponent) { + this.prefixesComponent = prefixesComponent; + } + public void dispose() { this.badgesComponent.dispose(); this.botsComponent.dispose(); @@ -119,6 +134,7 @@ public class HabboInventory { this.itemsComponent.dispose(); this.petsComponent.dispose(); this.wardrobeComponent.dispose(); + this.prefixesComponent.dispose(); this.badgesComponent = null; this.botsComponent = null; @@ -126,6 +142,7 @@ public class HabboInventory { this.itemsComponent = null; this.petsComponent = null; this.wardrobeComponent = null; + this.prefixesComponent = null; } public void addMarketplaceOffer(MarketPlaceOffer marketPlaceOffer) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboItem.java index ee85190c..ec9ed53e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboItem.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboItem.java @@ -45,6 +45,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers { private int id; private int userId; + private int databaseUserId; private int roomId; private Item baseItem; private String wallPosition; @@ -62,6 +63,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers { public HabboItem(ResultSet set, Item baseItem) throws SQLException { this.id = set.getInt("id"); this.userId = set.getInt("user_id"); + this.databaseUserId = this.userId; this.roomId = set.getInt("room_id"); this.baseItem = baseItem; this.wallPosition = set.getString("wall_pos"); @@ -81,6 +83,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers { public HabboItem(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { this.id = id; this.userId = userId; + this.databaseUserId = userId; this.roomId = 0; this.baseItem = item; this.wallPosition = ""; @@ -133,6 +136,24 @@ public abstract class HabboItem implements Runnable, IEventTriggers { serverMessage.appendInt(-1); serverMessage.appendInt(this.isUsable()); serverMessage.appendInt(this.getUserId()); + serverMessage.appendInt(this.getBaseItem().allowStack() ? 1 : 0); + serverMessage.appendInt(this.getBaseItem().allowSit() ? 1 : 0); + serverMessage.appendInt(this.getBaseItem().allowLay() ? 1 : 0); + serverMessage.appendInt(this.getBaseItem().allowWalk() ? 1 : 0); + serverMessage.appendInt(this.getBaseItem().getWidth()); + serverMessage.appendInt(this.getBaseItem().getLength()); + serverMessage.appendInt(this.getTeleportTargetId()); + } + + public int getTeleportTargetId() { + if (!(InteractionTeleport.class.isAssignableFrom(this.getBaseItem().getInteractionType().getType()) + || InteractionTeleportTile.class.isAssignableFrom(this.getBaseItem().getInteractionType().getType()))) { + return 0; + } + + int[] target = Emulator.getGameEnvironment().getItemManager().getTargetTeleportRoomId(this); + + return (target.length >= 2) ? target[1] : 0; } public int getId() { @@ -151,6 +172,11 @@ public abstract class HabboItem implements Runnable, IEventTriggers { public void setUserId(int userId) { this.userId = userId; + this.databaseUserId = userId; + } + + public void setVirtualUserId(int userId) { + this.userId = userId; } public int getRoomId() { @@ -257,7 +283,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers { } } else if (this.needsUpdate) { try (PreparedStatement statement = connection.prepareStatement("UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?")) { - statement.setInt(1, this.userId); + statement.setInt(1, this.databaseUserId); statement.setInt(2, this.roomId); statement.setString(3, this.wallPosition); statement.setInt(4, this.x); @@ -294,7 +320,10 @@ public abstract class HabboItem implements Runnable, IEventTriggers { } } - if ((this.getBaseItem().getStateCount() > 1 && !(this instanceof InteractionDice)) || Arrays.asList(HabboItem.TOGGLING_INTERACTIONS).contains(this.getClass()) || (objects != null && objects.length == 1 && objects[0].equals("TOGGLE_OVERRIDE"))) { + boolean isTogglingInteraction = Arrays.stream(HabboItem.TOGGLING_INTERACTIONS) + .anyMatch(type -> type.isAssignableFrom(this.getClass())); + + if ((this.getBaseItem().getStateCount() > 1 && !(this instanceof InteractionDice)) || isTogglingInteraction || (objects != null && objects.length == 1 && objects[0].equals("TOGGLE_OVERRIDE"))) { WiredManager.triggerFurniStateChanged(room, client.getHabbo().getRoomUnit(), this); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboManager.java index af9f7368..14fd99bf 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboManager.java @@ -1,6 +1,7 @@ package com.eu.habbo.habbohotel.users; import com.eu.habbo.Emulator; +import com.eu.habbo.database.SqlQueries; import com.eu.habbo.habbohotel.modtool.ModToolBan; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.permissions.Rank; @@ -22,6 +23,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -35,55 +37,49 @@ public class HabboManager { public static boolean NAMECHANGE_ENABLED = false; private final ConcurrentHashMap onlineHabbos; + private final ConcurrentHashMap onlineHabbosByName; public HabboManager() { long millis = System.currentTimeMillis(); this.onlineHabbos = new ConcurrentHashMap<>(); + this.onlineHabbosByName = new ConcurrentHashMap<>(); LOGGER.info("Habbo Manager -> Loaded! ({} MS)", System.currentTimeMillis() - millis); } public static HabboInfo getOfflineHabboInfo(int id) { - HabboInfo info = null; - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE id = ? LIMIT 1")) { - statement.setInt(1, id); - try (ResultSet set = statement.executeQuery()) { - if (set.next()) { - info = new HabboInfo(set); - } - } - } catch (SQLException e) { + try { + return SqlQueries.queryOne( + "SELECT * FROM users WHERE id = ? LIMIT 1", + HabboInfo::new, + id).orElse(null); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); + return null; } - - return info; } public static HabboInfo getOfflineHabboInfo(String username) { - HabboInfo info = null; - - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE username = ? LIMIT 1")) { - statement.setString(1, username); - - try (ResultSet set = statement.executeQuery()) { - if (set.next()) { - info = new HabboInfo(set); - } - } - } catch (SQLException e) { + try { + return SqlQueries.queryOne( + "SELECT * FROM users WHERE username = ? LIMIT 1", + HabboInfo::new, + username).orElse(null); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); + return null; } - - return info; } public void addHabbo(Habbo habbo) { this.onlineHabbos.put(habbo.getHabboInfo().getId(), habbo); + this.onlineHabbosByName.put(habbo.getHabboInfo().getUsername().toLowerCase(), habbo); } public void removeHabbo(Habbo habbo) { this.onlineHabbos.remove(habbo.getHabboInfo().getId()); + this.onlineHabbosByName.remove(habbo.getHabboInfo().getUsername().toLowerCase()); } public Habbo getHabbo(int id) { @@ -91,14 +87,7 @@ public class HabboManager { } public Habbo getHabbo(String username) { - synchronized (this.onlineHabbos) { - for (Map.Entry map : this.onlineHabbos.entrySet()) { - if (map.getValue().getHabboInfo().getUsername().equalsIgnoreCase(username)) - return map.getValue(); - } - } - - return null; + return this.onlineHabbosByName.get(username.toLowerCase()); } public Habbo loadHabbo(String sso) { @@ -178,11 +167,9 @@ public class HabboManager { } public void sendPacketToHabbosWithPermission(ServerMessage message, String perm) { - synchronized (this.onlineHabbos) { - for (Habbo habbo : this.onlineHabbos.values()) { - if (habbo.hasPermission(perm)) { - habbo.getClient().sendResponse(message); - } + for (Habbo habbo : this.onlineHabbos.values()) { + if (habbo.hasPermission(perm)) { + habbo.getClient().sendResponse(message); } } } @@ -200,43 +187,32 @@ public class HabboManager { LOGGER.info("Habbo Manager -> Disposed!"); } - public ArrayList getCloneAccounts(Habbo habbo, int limit) { - ArrayList habboInfo = new ArrayList<>(); - - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE ip_register = ? OR ip_current = ? AND id != ? ORDER BY id DESC LIMIT ?")) { - statement.setString(1, habbo.getHabboInfo().getIpRegister()); - statement.setString(2, habbo.getHabboInfo().getIpLogin()); - statement.setInt(3, habbo.getHabboInfo().getId()); - statement.setInt(4, limit); - - try (ResultSet set = statement.executeQuery()) { - while (set.next()) { - habboInfo.add(new HabboInfo(set)); - } - } - } catch (SQLException e) { + public List getCloneAccounts(Habbo habbo, int limit) { + try { + return SqlQueries.query( + "SELECT * FROM users WHERE (ip_register = ? OR ip_current = ?) AND id != ? ORDER BY id DESC LIMIT ?", + HabboInfo::new, + habbo.getHabboInfo().getIpRegister(), + habbo.getHabboInfo().getIpLogin(), + habbo.getHabboInfo().getId(), + limit); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); + return new ArrayList<>(); } - - return habboInfo; } public List> getNameChanges(int userId, int limit) { - List> nameChanges = new ArrayList<>(); - - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT timestamp, new_name FROM namechange_log WHERE user_id = ? ORDER by timestamp DESC LIMIT ?")) { - statement.setInt(1, userId); - statement.setInt(2, limit); - try (ResultSet set = statement.executeQuery()) { - while (set.next()) { - nameChanges.add(new AbstractMap.SimpleEntry<>(set.getInt("timestamp"), set.getString("new_name"))); - } - } - } catch (SQLException e) { + try { + return SqlQueries.query( + "SELECT timestamp, new_name FROM namechange_log WHERE user_id = ? ORDER by timestamp DESC LIMIT ?", + rs -> new AbstractMap.SimpleEntry<>(rs.getInt("timestamp"), rs.getString("new_name")), + userId, + limit); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); + return Collections.emptyList(); } - - return nameChanges; } @@ -282,11 +258,9 @@ public class HabboManager { habbo.getClient().sendResponse(new RecyclerLogicComposer()); habbo.alert(Emulator.getTexts().getValue("commands.generic.cmd_give_rank.new_rank").replace("id", newRank.getName())); } else { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users SET `rank` = ? WHERE id = ? LIMIT 1")) { - statement.setInt(1, rankId); - statement.setInt(2, userId); - statement.execute(); - } catch (SQLException e) { + try { + SqlQueries.update("UPDATE users SET `rank` = ? WHERE id = ? LIMIT 1", rankId, userId); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); } } @@ -299,11 +273,9 @@ public class HabboManager { if (habbo != null) { habbo.giveCredits(credits); } else { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users SET credits = credits + ? WHERE id = ? LIMIT 1")) { - statement.setInt(1, credits); - statement.setInt(2, userId); - statement.execute(); - } catch (SQLException e) { + try { + SqlQueries.update("UPDATE users SET credits = credits + ? WHERE id = ? LIMIT 1", credits, userId); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java index 74d59e75..b760e4de 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java @@ -99,6 +99,7 @@ public class HabboStats implements Runnable { public int maxRooms; public int lastHCPayday; public int hcGiftsClaimed; + public int buildersClubBonusFurni; public int hcMessageLastModified = Emulator.getIntUnixTimestamp(); public THashSet subscriptions; @@ -155,6 +156,7 @@ public class HabboStats implements Runnable { this.maxRooms = set.getInt("max_rooms"); this.lastHCPayday = set.getInt("last_hc_payday"); this.hcGiftsClaimed = set.getInt("hc_gifts_claimed"); + this.buildersClubBonusFurni = set.getInt("builders_club_bonus_furni"); this.nuxReward = this.nux; @@ -327,7 +329,7 @@ public class HabboStats implements Runnable { int onlineTime = Emulator.getIntUnixTimestamp() - onlineTimeLast; try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { - try (PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET achievement_score = ?, respects_received = ?, respects_given = ?, daily_respect_points = ?, block_following = ?, block_friendrequests = ?, online_time = online_time + ?, guild_id = ?, daily_pet_respect_points = ?, club_expire_timestamp = ?, login_streak = ?, rent_space_id = ?, rent_space_endtime = ?, volume_system = ?, volume_furni = ?, volume_trax = ?, block_roominvites = ?, old_chat = ?, block_camera_follow = ?, chat_color = ?, hof_points = ?, block_alerts = ?, talent_track_citizenship_level = ?, talent_track_helpers_level = ?, ignore_bots = ?, ignore_pets = ?, nux = ?, mute_end_timestamp = ?, allow_name_change = ?, perk_trade = ?, can_trade = ?, `forums_post_count` = ?, ui_flags = ?, has_gotten_default_saved_searches = ?, max_friends = ?, max_rooms = ?, last_hc_payday = ?, hc_gifts_claimed = ? WHERE user_id = ? LIMIT 1")) { + try (PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET achievement_score = ?, respects_received = ?, respects_given = ?, daily_respect_points = ?, block_following = ?, block_friendrequests = ?, online_time = online_time + ?, guild_id = ?, daily_pet_respect_points = ?, club_expire_timestamp = ?, login_streak = ?, rent_space_id = ?, rent_space_endtime = ?, volume_system = ?, volume_furni = ?, volume_trax = ?, block_roominvites = ?, old_chat = ?, block_camera_follow = ?, chat_color = ?, hof_points = ?, block_alerts = ?, talent_track_citizenship_level = ?, talent_track_helpers_level = ?, ignore_bots = ?, ignore_pets = ?, nux = ?, mute_end_timestamp = ?, allow_name_change = ?, perk_trade = ?, can_trade = ?, `forums_post_count` = ?, ui_flags = ?, has_gotten_default_saved_searches = ?, max_friends = ?, max_rooms = ?, last_hc_payday = ?, hc_gifts_claimed = ?, builders_club_bonus_furni = ? WHERE user_id = ? LIMIT 1")) { statement.setInt(1, this.achievementScore); statement.setInt(2, this.respectPointsReceived); statement.setInt(3, this.respectPointsGiven); @@ -366,7 +368,8 @@ public class HabboStats implements Runnable { statement.setInt(36, this.maxRooms); statement.setInt(37, this.lastHCPayday); statement.setInt(38, this.hcGiftsClaimed); - statement.setInt(39, this.habboInfo.getId()); + statement.setInt(39, this.buildersClubBonusFurni); + statement.setInt(40, this.habboInfo.getId()); statement.executeUpdate(); } @@ -436,6 +439,10 @@ public class HabboStats implements Runnable { } public int getAchievementProgress(Achievement achievement) { + if (achievement == null) { + return 0; + } + if (this.achievementProgress.containsKey(achievement)) return this.achievementProgress.get(achievement); @@ -575,6 +582,18 @@ public class HabboStats implements Runnable { return totalGifts - this.hcGiftsClaimed; } + public int getBuildersClubBonusFurni() { + return this.buildersClubBonusFurni; + } + + public void addBuildersClubBonusFurni(int amount) { + if (amount <= 0) { + return; + } + + this.buildersClubBonusFurni += amount; + } + public THashMap getAchievementProgress() { return this.achievementProgress; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserPrefix.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserPrefix.java new file mode 100644 index 00000000..6879d1be --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/UserPrefix.java @@ -0,0 +1,122 @@ +package com.eu.habbo.habbohotel.users; + +import com.eu.habbo.Emulator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.*; + +public class UserPrefix implements Runnable { + private static final Logger LOGGER = LoggerFactory.getLogger(UserPrefix.class); + + private int id; + private final int userId; + private String text; + private String color; + private String icon; + private String effect; + private boolean active; + private boolean needsInsert; + private boolean needsUpdate; + private boolean needsDelete; + + public UserPrefix(ResultSet set) throws SQLException { + this.id = set.getInt("id"); + this.userId = set.getInt("user_id"); + this.text = set.getString("text"); + this.color = set.getString("color"); + this.icon = set.getString("icon"); + if (this.icon == null) this.icon = ""; + this.effect = set.getString("effect"); + if (this.effect == null) this.effect = ""; + this.active = set.getBoolean("active"); + this.needsInsert = false; + this.needsUpdate = false; + this.needsDelete = false; + } + + public UserPrefix(int userId, String text, String color, String icon, String effect) { + this.id = 0; + this.userId = userId; + this.text = text; + this.color = color; + this.icon = icon != null ? icon : ""; + this.effect = effect != null ? effect : ""; + this.active = false; + this.needsInsert = true; + this.needsUpdate = false; + this.needsDelete = false; + } + + @Override + public void run() { + try { + if (this.needsInsert) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO user_prefixes (user_id, text, color, icon, effect, active) VALUES (?, ?, ?, ?, ?, ?)", + Statement.RETURN_GENERATED_KEYS)) { + statement.setInt(1, this.userId); + statement.setString(2, this.text); + statement.setString(3, this.color); + statement.setString(4, this.icon); + statement.setString(5, this.effect); + statement.setBoolean(6, this.active); + statement.execute(); + try (ResultSet set = statement.getGeneratedKeys()) { + if (set.next()) { + this.id = set.getInt(1); + } + } + } + this.needsInsert = false; + } else if (this.needsDelete) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "DELETE FROM user_prefixes WHERE id = ? AND user_id = ?")) { + statement.setInt(1, this.id); + statement.setInt(2, this.userId); + statement.execute(); + } + this.needsDelete = false; + } else if (this.needsUpdate) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "UPDATE user_prefixes SET text = ?, color = ?, icon = ?, effect = ?, active = ? WHERE id = ? AND user_id = ?")) { + statement.setString(1, this.text); + statement.setString(2, this.color); + statement.setString(3, this.icon); + statement.setString(4, this.effect); + statement.setBoolean(5, this.active); + statement.setInt(6, this.id); + statement.setInt(7, this.userId); + statement.execute(); + } + this.needsUpdate = false; + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + } + + public int getId() { return this.id; } + public int getUserId() { return this.userId; } + public String getText() { return this.text; } + public void setText(String text) { this.text = text; } + public String getColor() { return this.color; } + public void setColor(String color) { this.color = color; } + public String getIcon() { return this.icon; } + public void setIcon(String icon) { this.icon = icon != null ? icon : ""; } + public String getEffect() { return this.effect; } + public void setEffect(String effect) { this.effect = effect != null ? effect : ""; } + public boolean isActive() { return this.active; } + + public void setActive(boolean active) { + this.active = active; + this.needsUpdate = true; + } + + public void needsUpdate(boolean needsUpdate) { this.needsUpdate = needsUpdate; } + public void needsInsert(boolean needsInsert) { this.needsInsert = needsInsert; } + public void needsDelete(boolean needsDelete) { this.needsDelete = needsDelete; } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/ItemsComponent.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/ItemsComponent.java index 7a49a166..8f496d17 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/ItemsComponent.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/ItemsComponent.java @@ -1,6 +1,7 @@ package com.eu.habbo.habbohotel.users.inventory; import com.eu.habbo.Emulator; +import com.eu.habbo.database.SqlQueries; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboInventory; @@ -9,7 +10,6 @@ import com.eu.habbo.plugin.events.inventory.InventoryItemAddedEvent; import com.eu.habbo.plugin.events.inventory.InventoryItemRemovedEvent; import com.eu.habbo.plugin.events.inventory.InventoryItemsAddedEvent; import gnu.trove.TCollections; -import gnu.trove.iterator.TIntObjectIterator; import gnu.trove.map.TIntObjectMap; import gnu.trove.map.hash.THashMap; import gnu.trove.map.hash.TIntObjectHashMap; @@ -18,11 +18,9 @@ import gnu.trove.set.hash.THashSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; -import java.util.NoSuchElementException; +import java.util.ArrayList; +import java.util.List; public class ItemsComponent { private static final Logger LOGGER = LoggerFactory.getLogger(ItemsComponent.class); @@ -39,25 +37,23 @@ public class ItemsComponent { public static THashMap loadItems(Habbo habbo) { THashMap itemsList = new THashMap<>(); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM items WHERE room_id = ? AND user_id = ?")) { - statement.setInt(1, 0); - statement.setInt(2, habbo.getHabboInfo().getId()); - try (ResultSet set = statement.executeQuery()) { - while (set.next()) { - try { - HabboItem item = Emulator.getGameEnvironment().getItemManager().loadHabboItem(set); - - if (item != null) { - itemsList.put(set.getInt("id"), item); - } else { - LOGGER.error("Failed to load HabboItem: {}", set.getInt("id")); + try { + SqlQueries.forEach( + "SELECT * FROM items WHERE room_id = ? AND user_id = ?", + rs -> { + try { + HabboItem item = Emulator.getGameEnvironment().getItemManager().loadHabboItem(rs); + if (item != null) { + itemsList.put(rs.getInt("id"), item); + } else { + LOGGER.error("Failed to load HabboItem: {}", rs.getInt("id")); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } - } - } - } catch (SQLException e) { + }, + 0, habbo.getHabboInfo().getId()); + } catch (SqlQueries.DataAccessException e) { LOGGER.error("Caught SQL exception", e); } @@ -151,22 +147,46 @@ public class ItemsComponent { public void dispose() { synchronized (this.items) { - TIntObjectIterator items = this.items.iterator(); - - if (items == null) { - LOGGER.error("Items is NULL!"); - return; - } - if (!this.items.isEmpty()) { - for (int i = this.items.size(); i-- > 0; ) { - try { - items.advance(); - } catch (NoSuchElementException e) { - break; + List updates = new ArrayList<>(); + List deletes = new ArrayList<>(); + for (HabboItem item : this.items.valueCollection()) { + if (item.needsDelete()) { + deletes.add(item); + item.needsUpdate(false); + item.needsDelete(false); + } else if (item.needsUpdate()) { + updates.add(item); + item.needsUpdate(false); } - if (items.value().needsUpdate()) - Emulator.getThreading().run(items.value()); + } + + try { + if (!deletes.isEmpty()) { + SqlQueries.batchUpdate( + "DELETE FROM items WHERE id = ?", + deletes, + (ps, item) -> ps.setInt(1, item.getId())); + } + if (!updates.isEmpty()) { + SqlQueries.batchUpdate( + "UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?", + updates, + (ps, item) -> { + ps.setInt(1, item.getUserId()); + ps.setInt(2, item.getRoomId()); + ps.setString(3, item.getWallPosition()); + ps.setInt(4, item.getX()); + ps.setInt(5, item.getY()); + ps.setDouble(6, item.getZ()); + ps.setInt(7, item.getRotation()); + ps.setString(8, item.getExtradata()); + ps.setString(9, item.getLimitedStack() + ":" + item.getLimitedSells()); + ps.setInt(10, item.getId()); + }); + } + } catch (SqlQueries.DataAccessException e) { + LOGGER.error("Caught SQL exception during batch item save", e); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/PrefixesComponent.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/PrefixesComponent.java new file mode 100644 index 00000000..28889ede --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/inventory/PrefixesComponent.java @@ -0,0 +1,105 @@ +package com.eu.habbo.habbohotel.users.inventory; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserPrefix; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class PrefixesComponent { + private static final Logger LOGGER = LoggerFactory.getLogger(PrefixesComponent.class); + + private final List prefixes = new ArrayList<>(); + private final Habbo habbo; + + public PrefixesComponent(Habbo habbo) { + this.habbo = habbo; + this.loadPrefixes(); + } + + private void loadPrefixes() { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT * FROM user_prefixes WHERE user_id = ?")) { + statement.setInt(1, this.habbo.getHabboInfo().getId()); + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + this.prefixes.add(new UserPrefix(set)); + } + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + } + + public List getPrefixes() { + synchronized (this.prefixes) { + return new ArrayList<>(this.prefixes); + } + } + + public UserPrefix getActivePrefix() { + synchronized (this.prefixes) { + for (UserPrefix prefix : this.prefixes) { + if (prefix.isActive()) return prefix; + } + } + return null; + } + + public UserPrefix getPrefix(int id) { + synchronized (this.prefixes) { + for (UserPrefix prefix : this.prefixes) { + if (prefix.getId() == id) return prefix; + } + } + return null; + } + + public void addPrefix(UserPrefix prefix) { + synchronized (this.prefixes) { + this.prefixes.add(prefix); + } + } + + public void removePrefix(UserPrefix prefix) { + synchronized (this.prefixes) { + this.prefixes.remove(prefix); + } + } + + public void setActive(int prefixId) { + synchronized (this.prefixes) { + for (UserPrefix prefix : this.prefixes) { + boolean shouldBeActive = prefix.getId() == prefixId; + if (prefix.isActive() != shouldBeActive) { + prefix.setActive(shouldBeActive); + Emulator.getThreading().run(prefix); + } + } + } + } + + public void deactivateAll() { + synchronized (this.prefixes) { + for (UserPrefix prefix : this.prefixes) { + if (prefix.isActive()) { + prefix.setActive(false); + Emulator.getThreading().run(prefix); + } + } + } + } + + public void dispose() { + synchronized (this.prefixes) { + this.prefixes.clear(); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/Subscription.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/Subscription.java index 08fba035..96ede18c 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/Subscription.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/Subscription.java @@ -11,6 +11,7 @@ import java.sql.SQLException; */ public class Subscription { public static final String HABBO_CLUB = "HABBO_CLUB"; + public static final String BUILDERS_CLUB = "BUILDERS_CLUB"; private final int id; private final int userId; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionBuildersClub.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionBuildersClub.java new file mode 100644 index 00000000..d25eb4c3 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionBuildersClub.java @@ -0,0 +1,50 @@ +package com.eu.habbo.habbohotel.users.subscriptions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; +import com.eu.habbo.habbohotel.users.Habbo; + +public class SubscriptionBuildersClub extends Subscription { + public SubscriptionBuildersClub(Integer id, Integer userId, String subscriptionType, Integer timestampStart, Integer duration, Boolean active) { + super(id, userId, subscriptionType, timestampStart, duration, active); + } + + @Override + public void onCreated() { + super.onCreated(); + if (BuildersClubRoomSupport.syncOwnedRooms(this.getUserId()) > 0) { + BuildersClubRoomSupport.sendRoomUnlockedBubble(this.getUserId()); + } + BuildersClubRoomSupport.sendMembershipMadeBubble(this.getUserId()); + this.sendStatus(); + } + + @Override + public void onExtended(int duration) { + super.onExtended(duration); + if (BuildersClubRoomSupport.syncOwnedRooms(this.getUserId()) > 0) { + BuildersClubRoomSupport.sendRoomUnlockedBubble(this.getUserId()); + } + BuildersClubRoomSupport.sendMembershipExtendedBubble(this.getUserId()); + this.sendStatus(); + } + + @Override + public void onExpired() { + super.onExpired(); + BuildersClubRoomSupport.syncOwnedRooms(this.getUserId()); + BuildersClubRoomSupport.sendMembershipExpiredAlert( + this.getUserId(), + BuildersClubRoomSupport.hasTrackedItemsInOwnedRooms(this.getUserId()) + ); + this.sendStatus(); + } + + private void sendStatus() { + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(this.getUserId()); + + if (habbo != null && habbo.getClient() != null) { + BuildersClubRoomSupport.sendPlacementStatus(habbo); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionHabboClub.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionHabboClub.java index 5e1efddd..83716292 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionHabboClub.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionHabboClub.java @@ -14,7 +14,6 @@ import com.eu.habbo.messages.outgoing.catalog.ClubCenterDataComposer; import com.eu.habbo.messages.outgoing.generic.PickMonthlyClubGiftNotificationComposer; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer; import com.eu.habbo.messages.outgoing.users.*; -import gnu.trove.map.hash.THashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,6 +23,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.HashMap; import java.util.Map; import java.util.TreeMap; @@ -215,7 +215,7 @@ public class SubscriptionHabboClub extends Subscription { } } - THashMap queryParams = new THashMap<>(); + Map queryParams = new HashMap<>(); queryParams.put("@user_id", habbo.getId()); queryParams.put("@timestamp_start", habbo.getHabboStats().lastHCPayday); queryParams.put("@timestamp_end", HC_PAYDAY_NEXT_DATE); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionManager.java index b39d9d52..333eee44 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/subscriptions/SubscriptionManager.java @@ -28,6 +28,7 @@ public class SubscriptionManager { public void init() { this.types.put(Subscription.HABBO_CLUB, SubscriptionHabboClub.class); + this.types.put(Subscription.BUILDERS_CLUB, SubscriptionBuildersClub.class); } public void addSubscriptionType(String type, Class clazz) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredConditionType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredConditionType.java index e620b4e0..5776b295 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredConditionType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredConditionType.java @@ -25,7 +25,24 @@ public enum WiredConditionType { NOT_ACTOR_WEARS_EFFECT(23), DATE_RANGE(24), ACTOR_HAS_HANDITEM(25), - MOVEMENT_VALIDATION(26); // i dont know what type it is but its needed + MOVEMENT_VALIDATION(26), // i dont know what type it is but its needed + COUNTER_TIME_MATCHES(27), + USER_PERFORMS_ACTION(28), + HAS_ALTITUDE(29), + NOT_USER_PERFORMS_ACTION(30), + NOT_ACTOR_HAS_HANDITEM(31), + TRIGGERER_MATCH(32), + NOT_TRIGGERER_MATCH(33), + TEAM_HAS_SCORE(34), + TEAM_HAS_RANK(35), + MATCH_TIME(36), + MATCH_DATE(37), + ACTOR_DIR(38), + SLC_QUANTITY(39), + HAS_VAR(40), + NOT_HAS_VAR(41), + VAR_VAL_MATCH(42), + VAR_AGE_MATCH(43); public final int code; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java index 0d5e74d2..e80934c4 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredEffectType.java @@ -32,7 +32,36 @@ public enum WiredEffectType { FURNI_BYTYPE_SELECTOR(30), USERS_AREA_SELECTOR(31), USERS_NEIGHBORHOOD_SELECTOR(32), - SEND_SIGNAL(33); + SEND_SIGNAL(33), + FREEZE(34), + UNFREEZE(35), + FURNI_TO_USER(36), + USER_TO_FURNI(37), + FURNI_TO_FURNI(38), + SET_ALTITUDE(39), + RELATIVE_MOVE(40), + CONTROL_CLOCK(41), + ADJUST_CLOCK(42), + MOVE_ROTATE_USER(43), + FURNI_ALTITUDE_SELECTOR(44), + FURNI_ON_FURNI_SELECTOR(45), + FURNI_PICKS_SELECTOR(46), + FURNI_SIGNAL_SELECTOR(47), + USERS_SIGNAL_SELECTOR(48), + USERS_BY_TYPE_SELECTOR(49), + USERS_TEAM_SELECTOR(50), + USERS_BY_ACTION_SELECTOR(51), + USERS_BY_NAME_SELECTOR(52), + USERS_ON_FURNI_SELECTOR(53), + USERS_GROUP_SELECTOR(54), + USERS_HANDITEM_SELECTOR(55), + GIVE_VAR(69), + REMOVE_VAR(73), + CHANGE_VAR_VAL(74), + FURNI_WITH_VAR_SELECTOR(75), + USERS_WITH_VAR_SELECTOR(76), + NEG_CALL_STACKS(86), + NEG_SEND_SIGNAL(87); public final int code; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java index 6aab1031..a0451992 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java @@ -10,9 +10,13 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.WiredTriggerReset; import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectGiveReward; import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectTriggerStacks; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecuteInOrder; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecutionLimit; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraOrEval; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRandom; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUnseen; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomWiredDisableSupport; import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.Habbo; @@ -27,6 +31,7 @@ import com.eu.habbo.plugin.events.furniture.wired.WiredConditionFailedEvent; import com.eu.habbo.plugin.events.furniture.wired.WiredStackExecutedEvent; import com.eu.habbo.plugin.events.furniture.wired.WiredStackTriggeredEvent; import com.eu.habbo.plugin.events.users.UserWiredRewardReceived; +import com.eu.habbo.habbohotel.wired.core.WiredExecutionOrderUtil; import com.google.gson.GsonBuilder; import gnu.trove.set.hash.THashSet; import org.slf4j.Logger; @@ -37,8 +42,10 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; -import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; public class WiredHandler { private static final Logger LOGGER = LoggerFactory.getLogger(WiredHandler.class); @@ -49,6 +56,11 @@ public class WiredHandler { private static GsonBuilder gsonBuilder = null; + private static final class LegacyExecutionPlan { + private final LinkedHashSet effects = new LinkedHashSet<>(); + private boolean executeInOrder = false; + } + public static boolean handle(WiredTriggerType triggerType, RoomUnit roomUnit, Room room, Object[] stuff) { if (triggerType == WiredTriggerType.CUSTOM) return false; @@ -60,6 +72,9 @@ public class WiredHandler { if (room == null) return false; + if (RoomWiredDisableSupport.isWiredDisabled(room)) + return false; + if (!room.isLoaded()) return false; @@ -72,7 +87,7 @@ public class WiredHandler { return false; long millis = System.currentTimeMillis(); - THashSet effectsToExecute = new THashSet(); + List executionPlans = new ArrayList<>(); List triggeredTiles = new ArrayList<>(); for (InteractionWiredTrigger trigger : triggers) { @@ -81,10 +96,10 @@ public class WiredHandler { if (triggeredTiles.contains(tile)) continue; - THashSet tEffectsToExecute = new THashSet(); + LegacyExecutionPlan executionPlan = new LegacyExecutionPlan(); - if (handle(trigger, roomUnit, room, stuff, tEffectsToExecute)) { - effectsToExecute.addAll(tEffectsToExecute); + if (handle(trigger, roomUnit, room, stuff, executionPlan)) { + executionPlans.add(executionPlan); if (triggerType.equals(WiredTriggerType.SAY_SOMETHING)) talked = true; @@ -93,8 +108,8 @@ public class WiredHandler { } } - for (InteractionWiredEffect effect : effectsToExecute) { - triggerEffect(effect, roomUnit, room, stuff, millis); + for (LegacyExecutionPlan executionPlan : executionPlans) { + triggerEffects(executionPlan.effects, roomUnit, room, stuff, millis, executionPlan.executeInOrder); } return talked; @@ -107,6 +122,9 @@ public class WiredHandler { if (room == null) return false; + if (RoomWiredDisableSupport.isWiredDisabled(room)) + return false; + if (!room.isLoaded()) return false; @@ -119,7 +137,7 @@ public class WiredHandler { return false; long millis = System.currentTimeMillis(); - THashSet effectsToExecute = new THashSet(); + List executionPlans = new ArrayList<>(); List triggeredTiles = new ArrayList<>(); for (InteractionWiredTrigger trigger : triggers) { @@ -130,95 +148,119 @@ public class WiredHandler { if (triggeredTiles.contains(tile)) continue; - THashSet tEffectsToExecute = new THashSet(); + LegacyExecutionPlan executionPlan = new LegacyExecutionPlan(); - if (handle(trigger, roomUnit, room, stuff, tEffectsToExecute)) { - effectsToExecute.addAll(tEffectsToExecute); + if (handle(trigger, roomUnit, room, stuff, executionPlan)) { + executionPlans.add(executionPlan); triggeredTiles.add(tile); } } - for (InteractionWiredEffect effect : effectsToExecute) { - triggerEffect(effect, roomUnit, room, stuff, millis); + for (LegacyExecutionPlan executionPlan : executionPlans) { + triggerEffects(executionPlan.effects, roomUnit, room, stuff, millis, executionPlan.executeInOrder); } - return effectsToExecute.size() > 0; + return !executionPlans.isEmpty(); } public static boolean handle(InteractionWiredTrigger trigger, final RoomUnit roomUnit, final Room room, final Object[] stuff) { long millis = System.currentTimeMillis(); - THashSet effectsToExecute = new THashSet(); + LegacyExecutionPlan executionPlan = new LegacyExecutionPlan(); - if(handle(trigger, roomUnit, room, stuff, effectsToExecute)) { - for (InteractionWiredEffect effect : effectsToExecute) { - triggerEffect(effect, roomUnit, room, stuff, millis); - } + if (RoomWiredDisableSupport.isWiredDisabled(room)) + return false; + + if(handle(trigger, roomUnit, room, stuff, executionPlan)) { + triggerEffects(executionPlan.effects, roomUnit, room, stuff, millis, executionPlan.executeInOrder); return true; } return false; } - public static boolean handle(InteractionWiredTrigger trigger, final RoomUnit roomUnit, final Room room, final Object[] stuff, final THashSet effectsToExecute) { + private static boolean handle(InteractionWiredTrigger trigger, final RoomUnit roomUnit, final Room room, final Object[] stuff, final LegacyExecutionPlan executionPlan) { long millis = System.currentTimeMillis(); int roomUnitId = roomUnit != null ? roomUnit.getId() : -1; if (Emulator.isReady && ((Emulator.getConfig().getBoolean("wired.custom.enabled", false) && (trigger.canExecute(millis) || roomUnitId > -1) && trigger.userCanExecute(roomUnitId, millis)) || (!Emulator.getConfig().getBoolean("wired.custom.enabled", false) && trigger.canExecute(millis))) && trigger.execute(roomUnit, room, stuff)) { - trigger.activateBox(room, roomUnit, millis); - THashSet conditions = room.getRoomSpecialTypes().getConditions(trigger.getX(), trigger.getY()); THashSet effects = room.getRoomSpecialTypes().getEffects(trigger.getX(), trigger.getY()); - if (Emulator.getPluginManager().fireEvent(new WiredStackTriggeredEvent(room, roomUnit, trigger, effects, conditions)).isCancelled()) - return false; + THashSet extras = room.getRoomSpecialTypes().getExtras(trigger.getX(), trigger.getY()); + WiredExtraExecutionLimit executionLimitExtra = null; + WiredExtraRandom randomExtra = null; - if (!conditions.isEmpty()) { - ArrayList matchedConditions = new ArrayList<>(conditions.size()); - for (InteractionWiredCondition searchMatched : conditions) { - if (!matchedConditions.contains(searchMatched.getType()) && searchMatched.operator() == WiredConditionOperator.OR && searchMatched.execute(roomUnit, room, stuff)) { - matchedConditions.add(searchMatched.getType()); - } + for (InteractionWiredExtra extra : extras) { + if (executionLimitExtra == null && extra instanceof WiredExtraExecutionLimit) { + executionLimitExtra = (WiredExtraExecutionLimit) extra; } - for (InteractionWiredCondition condition : conditions) { - if (!((condition.operator() == WiredConditionOperator.OR && matchedConditions.contains(condition.getType())) || - (condition.operator() == WiredConditionOperator.AND && condition.execute(roomUnit, room, stuff))) && - !Emulator.getPluginManager().fireEvent(new WiredConditionFailedEvent(room, roomUnit, trigger, condition)).isCancelled()) { - - return false; - } + if (randomExtra == null && extra instanceof WiredExtraRandom) { + randomExtra = (WiredExtraRandom) extra; } } + if (!conditions.isEmpty()) { + int conditionEvaluationMode = WiredExtraOrEval.MODE_ALL; + int conditionEvaluationValue = 1; + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraOrEval) { + conditionEvaluationMode = ((WiredExtraOrEval) extra).getEvaluationMode(); + conditionEvaluationValue = ((WiredExtraOrEval) extra).getCompareValue(); + break; + } + } + + if (!evaluateConditions(conditions, roomUnit, room, stuff, conditionEvaluationMode, conditionEvaluationValue)) { + for (InteractionWiredCondition condition : conditions) { + if (!Emulator.getPluginManager().fireEvent(new WiredConditionFailedEvent(room, roomUnit, trigger, condition)).isCancelled()) { + break; + } + } + + return false; + } + } + + if (executionLimitExtra != null && !executionLimitExtra.tryAcquireExecutionSlot(millis)) { + return false; + } + + if (Emulator.getPluginManager().fireEvent(new WiredStackTriggeredEvent(room, roomUnit, trigger, effects, conditions)).isCancelled()) + return false; + + trigger.activateBox(room, roomUnit, millis); + trigger.setCooldown(millis); - boolean hasExtraRandom = room.getRoomSpecialTypes().hasExtraType(trigger.getX(), trigger.getY(), WiredExtraRandom.class); boolean hasExtraUnseen = room.getRoomSpecialTypes().hasExtraType(trigger.getX(), trigger.getY(), WiredExtraUnseen.class); - THashSet extras = room.getRoomSpecialTypes().getExtras(trigger.getX(), trigger.getY()); + boolean hasExtraExecuteInOrder = room.getRoomSpecialTypes().hasExtraType(trigger.getX(), trigger.getY(), WiredExtraExecuteInOrder.class); for (InteractionWiredExtra extra : extras) { extra.activateBox(room, roomUnit, millis); } - List effectList = new ArrayList<>(effects); - - if (hasExtraRandom || hasExtraUnseen) { - Collections.shuffle(effectList); - } + List effectList = (hasExtraUnseen || hasExtraExecuteInOrder) + ? WiredExecutionOrderUtil.sort(effects) + : new ArrayList<>(effects); + executionPlan.executeInOrder = hasExtraExecuteInOrder; if (hasExtraUnseen) { for (InteractionWiredExtra extra : room.getRoomSpecialTypes().getExtras(trigger.getX(), trigger.getY())) { if (extra instanceof WiredExtraUnseen) { extra.setExtradata(extra.getExtradata().equals("1") ? "0" : "1"); InteractionWiredEffect effect = ((WiredExtraUnseen) extra).getUnseenEffect(effectList); - effectsToExecute.add(effect); // triggerEffect(effect, roomUnit, room, stuff, millis); + if (effect != null) { + executionPlan.effects.add(effect); + } break; } } + } else if (randomExtra != null) { + executionPlan.effects.addAll(randomExtra.selectEffects(effectList)); + } else if (hasExtraExecuteInOrder) { + executionPlan.effects.addAll(effectList); } else { for (final InteractionWiredEffect effect : effectList) { - boolean executed = effectsToExecute.add(effect); //triggerEffect(effect, roomUnit, room, stuff, millis); - if (hasExtraRandom && executed) { - break; - } + executionPlan.effects.add(effect); } } @@ -228,12 +270,46 @@ public class WiredHandler { return false; } + private static boolean evaluateConditions(THashSet conditions, RoomUnit roomUnit, Room room, Object[] stuff, int evaluationMode, int evaluationValue) { + if (conditions == null || conditions.isEmpty()) { + return true; + } + + Map orGroupResults = new HashMap<>(); + int matchedRequirements = 0; + int totalRequirements = 0; + + for (InteractionWiredCondition condition : conditions) { + boolean result = condition.execute(roomUnit, room, stuff); + + if (condition.operator() == WiredConditionOperator.OR) { + orGroupResults.merge(condition.getType(), result, (left, right) -> left || right); + continue; + } + + totalRequirements++; + if (result) { + matchedRequirements++; + } + } + + totalRequirements += orGroupResults.size(); + + for (Boolean groupResult : orGroupResults.values()) { + if (Boolean.TRUE.equals(groupResult)) { + matchedRequirements++; + } + } + + return WiredExtraOrEval.matchesMode(evaluationMode, matchedRequirements, totalRequirements, evaluationValue); + } + private static boolean triggerEffect(InteractionWiredEffect effect, RoomUnit roomUnit, Room room, Object[] stuff, long millis) { boolean executed = false; if (effect != null && (effect.canExecute(millis) || (roomUnit != null && effect.requiresTriggeringUser() && Emulator.getConfig().getBoolean("wired.custom.enabled", false) && effect.userCanExecute(roomUnit.getId(), millis)))) { executed = true; if (!effect.requiresTriggeringUser() || (roomUnit != null && effect.requiresTriggeringUser())) { - Emulator.getThreading().run(() -> { + Runnable execution = () -> { if (room.isLoaded() && room.getHabbos().size() > 0) { try { if (!effect.execute(roomUnit, room, stuff)) return; @@ -244,13 +320,108 @@ public class WiredHandler { effect.activateBox(room, roomUnit, millis); } - }, effect.getDelay() * 500L); + }; + + long delayMs = effect.getDelay() * 500L; + long elapsedSinceTrigger = Math.max(0L, System.currentTimeMillis() - millis); + long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger); + + if (delayMs <= 0) { + execution.run(); + } else { + Emulator.getThreading().run(execution, remainingDelayMs); + } } } return executed; } + private static void triggerEffects(LinkedHashSet effects, RoomUnit roomUnit, Room room, Object[] stuff, long millis, boolean executeInOrder) { + if (effects == null || effects.isEmpty()) { + return; + } + + if (!executeInOrder) { + for (InteractionWiredEffect effect : effects) { + triggerEffect(effect, roomUnit, room, stuff, millis); + } + return; + } + + LinkedHashSet queueableEffects = new LinkedHashSet<>(); + + for (InteractionWiredEffect effect : effects) { + if (canQueueEffect(effect, roomUnit, millis)) { + queueableEffects.add(effect); + } + } + + LinkedHashSet delays = new LinkedHashSet<>(); + for (InteractionWiredEffect effect : queueableEffects) { + delays.add(effect.getDelay()); + } + + for (Integer delay : delays) { + List delayBatch = new ArrayList<>(); + + for (InteractionWiredEffect effect : queueableEffects) { + if (effect.getDelay() == delay) { + delayBatch.add(effect); + } + } + + if (delayBatch.isEmpty()) { + continue; + } + + if (delay > 0) { + long delayMs = delay * 500L; + long elapsedSinceTrigger = Math.max(0L, System.currentTimeMillis() - millis); + long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger); + Emulator.getThreading().run(() -> executeOrderedEffectBatch(delayBatch, roomUnit, room, stuff, millis), remainingDelayMs); + } else { + executeOrderedEffectBatch(delayBatch, roomUnit, room, stuff, millis); + } + } + } + + private static boolean canQueueEffect(InteractionWiredEffect effect, RoomUnit roomUnit, long millis) { + if (effect == null) { + return false; + } + + boolean canExecute = effect.canExecute(millis) + || (roomUnit != null && effect.requiresTriggeringUser() + && Emulator.getConfig().getBoolean("wired.custom.enabled", false) + && effect.userCanExecute(roomUnit.getId(), millis)); + + if (!canExecute) { + return false; + } + + return !effect.requiresTriggeringUser() || roomUnit != null; + } + + private static void executeOrderedEffectBatch(List effects, RoomUnit roomUnit, Room room, Object[] stuff, long millis) { + if (!room.isLoaded() || room.getHabbos().size() <= 0) { + return; + } + + for (InteractionWiredEffect effect : effects) { + try { + if (!effect.execute(roomUnit, room, stuff)) { + continue; + } + + effect.setCooldown(millis); + effect.activateBox(room, roomUnit, millis); + } catch (Exception e) { + LOGGER.error("Caught exception", e); + } + } + } + public static GsonBuilder getGsonBuilder() { if(gsonBuilder == null) { gsonBuilder = new GsonBuilder(); @@ -285,88 +456,115 @@ public class WiredHandler { } } - private static void giveReward(Habbo habbo, WiredEffectGiveReward wiredBox, WiredGiveRewardItem reward) { - if (wiredBox.limit > 0) - wiredBox.given++; - + private static void persistReward(int wiredId, int habboId, int rewardId, int timestamp) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO wired_rewards_given (wired_item, user_id, reward_id, timestamp) VALUES ( ?, ?, ?, ?)")) { - statement.setInt(1, wiredBox.getId()); - statement.setInt(2, habbo.getHabboInfo().getId()); - statement.setInt(3, reward.id); - statement.setInt(4, Emulator.getIntUnixTimestamp()); + statement.setInt(1, wiredId); + statement.setInt(2, habboId); + statement.setInt(3, rewardId); + statement.setInt(4, timestamp); statement.execute(); } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); } + } + private static void completeReward(Habbo habbo, WiredEffectGiveReward wiredBox, WiredGiveRewardItem reward, int successCode) { + if (wiredBox.limit > 0) + wiredBox.given++; + + persistReward(wiredBox.getId(), habbo.getHabboInfo().getId(), reward.id, Emulator.getIntUnixTimestamp()); + habbo.getClient().sendResponse(new WiredRewardAlertComposer(successCode)); + } + + private static boolean giveReward(Habbo habbo, WiredEffectGiveReward wiredBox, WiredGiveRewardItem reward) { if (reward.badge) { UserWiredRewardReceived rewardReceived = new UserWiredRewardReceived(habbo, wiredBox, "badge", reward.data); if (Emulator.getPluginManager().fireEvent(rewardReceived).isCancelled()) - return; + return false; if (rewardReceived.value.isEmpty()) - return; + return false; - if (habbo.getInventory().getBadgesComponent().hasBadge(rewardReceived.value)) - return; + if (habbo.getInventory().getBadgesComponent().hasBadge(rewardReceived.value)) { + habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED)); + return false; + } HabboBadge badge = new HabboBadge(0, rewardReceived.value, 0, habbo); Emulator.getThreading().run(badge); habbo.getInventory().getBadgesComponent().addBadge(badge); habbo.getClient().sendResponse(new AddUserBadgeComposer(badge)); - habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_RECEIVED_BADGE)); - } else { - String[] data = reward.data.split("#"); - - if (data.length == 2) { - UserWiredRewardReceived rewardReceived = new UserWiredRewardReceived(habbo, wiredBox, data[0], data[1]); - if (Emulator.getPluginManager().fireEvent(rewardReceived).isCancelled()) - return; - - if (rewardReceived.value.isEmpty()) - return; - - if (rewardReceived.type.equalsIgnoreCase("credits")) { - int credits = Integer.parseInt(rewardReceived.value); - habbo.giveCredits(credits); - } else if (rewardReceived.type.equalsIgnoreCase("pixels")) { - int pixels = Integer.parseInt(rewardReceived.value); - habbo.givePixels(pixels); - } else if (rewardReceived.type.startsWith("points")) { - int points = Integer.parseInt(rewardReceived.value); - int type = 5; - - try { - type = Integer.parseInt(rewardReceived.type.replace("points", "")); - } catch (Exception e) { - } - - habbo.givePoints(type, points); - } else if (rewardReceived.type.equalsIgnoreCase("furni")) { - Item baseItem = Emulator.getGameEnvironment().getItemManager().getItem(Integer.parseInt(rewardReceived.value)); - if (baseItem != null) { - HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(habbo.getHabboInfo().getId(), baseItem, 0, 0, ""); - - if (item != null) { - habbo.getClient().sendResponse(new AddHabboItemComposer(item)); - habbo.getClient().getHabbo().getInventory().getItemsComponent().addItem(item); - habbo.getClient().sendResponse(new PurchaseOKComposer(null)); - habbo.getClient().sendResponse(new InventoryRefreshComposer()); - habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_RECEIVED_ITEM)); - } - } - } else if (rewardReceived.type.equalsIgnoreCase("respect")) { - habbo.getHabboStats().respectPointsReceived += Integer.parseInt(rewardReceived.value); - } else if (rewardReceived.type.equalsIgnoreCase("cata")) { - CatalogItem item = Emulator.getGameEnvironment().getCatalogManager().getCatalogItem(Integer.parseInt(rewardReceived.value)); - - if (item != null) { - Emulator.getGameEnvironment().getCatalogManager().purchaseItem(null, item, habbo, 1, "", true); - } - habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_RECEIVED_ITEM)); - } - } + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_BADGE); + return true; } + + String[] data = reward.data.split("#"); + + if (data.length != 2) + return false; + + UserWiredRewardReceived rewardReceived = new UserWiredRewardReceived(habbo, wiredBox, data[0], data[1]); + if (Emulator.getPluginManager().fireEvent(rewardReceived).isCancelled()) + return false; + + if (rewardReceived.value.isEmpty()) + return false; + + if (rewardReceived.type.equalsIgnoreCase("credits")) { + habbo.giveCredits(Integer.parseInt(rewardReceived.value)); + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_ITEM); + return true; + } else if (rewardReceived.type.equalsIgnoreCase("diamonds") || rewardReceived.type.equalsIgnoreCase("diamond")) { + habbo.givePoints(5, Integer.parseInt(rewardReceived.value)); + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_ITEM); + return true; + } else if (rewardReceived.type.equalsIgnoreCase("pixels")) { + habbo.givePixels(Integer.parseInt(rewardReceived.value)); + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_ITEM); + return true; + } else if (rewardReceived.type.startsWith("points")) { + int points = Integer.parseInt(rewardReceived.value); + int type = 5; + + try { + type = Integer.parseInt(rewardReceived.type.replace("points", "")); + } catch (Exception e) { + } + + habbo.givePoints(type, points); + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_ITEM); + return true; + } else if (rewardReceived.type.equalsIgnoreCase("furni")) { + Item baseItem = Emulator.getGameEnvironment().getItemManager().getItem(Integer.parseInt(rewardReceived.value)); + if (baseItem == null) + return false; + + HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(habbo.getHabboInfo().getId(), baseItem, 0, 0, ""); + if (item == null) + return false; + + habbo.getClient().sendResponse(new AddHabboItemComposer(item)); + habbo.getClient().getHabbo().getInventory().getItemsComponent().addItem(item); + habbo.getClient().sendResponse(new PurchaseOKComposer(null)); + habbo.getClient().sendResponse(new InventoryRefreshComposer()); + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_ITEM); + return true; + } else if (rewardReceived.type.equalsIgnoreCase("respect")) { + habbo.getHabboStats().respectPointsReceived += Integer.parseInt(rewardReceived.value); + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_ITEM); + return true; + } else if (rewardReceived.type.equalsIgnoreCase("cata")) { + CatalogItem item = Emulator.getGameEnvironment().getCatalogManager().getCatalogItem(Integer.parseInt(rewardReceived.value)); + + if (item == null) + return false; + + Emulator.getGameEnvironment().getCatalogManager().purchaseItem(null, item, habbo, 1, "", true); + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_ITEM); + return true; + } + + return false; } public static boolean getReward(Habbo habbo, WiredEffectGiveReward wiredBox) { @@ -433,22 +631,26 @@ public class WiredHandler { } if (!found) { - giveReward(habbo, wiredBox, item); - return true; + return giveReward(habbo, wiredBox, item); } } + + habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALL_COLLECTED)); + return false; } else { int randomNumber = Emulator.getRandom().nextInt(101); int count = 0; for (WiredGiveRewardItem item : wiredBox.rewardItems) { if (randomNumber >= count && randomNumber <= (count + item.probability)) { - giveReward(habbo, wiredBox, item); - return true; + return giveReward(habbo, wiredBox, item); } count += item.probability; } + + habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.UNLUCKY_NO_REWARD)); + return false; } } } @@ -466,7 +668,7 @@ public class WiredHandler { room.getRoomSpecialTypes().getTriggers().forEach(t -> { if (t == null) return; - if (t.getType() == WiredTriggerType.AT_GIVEN_TIME || t.getType() == WiredTriggerType.PERIODICALLY || t.getType() == WiredTriggerType.PERIODICALLY_LONG) { + if (t.getType() == WiredTriggerType.AT_GIVEN_TIME || t.getType() == WiredTriggerType.PERIODICALLY || t.getType() == WiredTriggerType.PERIODICALLY_LONG || t.getType() == WiredTriggerType.PERIODICALLY_SHORT) { ((WiredTriggerReset) t).resetTimer(); } }); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredMatchFurniSetting.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredMatchFurniSetting.java index 7771450b..5179e503 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredMatchFurniSetting.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredMatchFurniSetting.java @@ -6,13 +6,19 @@ public class WiredMatchFurniSetting { public final int rotation; public final int x; public final int y; + public final double z; public WiredMatchFurniSetting(int itemId, String state, int rotation, int x, int y) { + this(itemId, state, rotation, x, y, 0.0D); + } + + public WiredMatchFurniSetting(int itemId, String state, int rotation, int x, int y, double z) { this.item_id = itemId; this.state = state.replace("\t\t\t", " "); this.rotation = rotation; this.x = x; this.y = y; + this.z = z; } @Override @@ -21,7 +27,7 @@ public class WiredMatchFurniSetting { } public String toString(boolean includeState) { - return this.item_id + "-" + (this.state.isEmpty() || !includeState ? " " : this.state) + "-" + this.rotation + "-" + this.x + "-" + this.y; + return this.item_id + "-" + (this.state.isEmpty() || !includeState ? " " : this.state) + "-" + this.rotation + "-" + this.x + "-" + this.y + "-" + this.z; } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredTriggerType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredTriggerType.java index e1767c1b..db753325 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredTriggerType.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredTriggerType.java @@ -15,6 +15,14 @@ public enum WiredTriggerType { PERIODICALLY_LONG(12), BOT_REACHED_STF(13), BOT_REACHED_AVTR(14), + LEAVE_ROOM(16), + PERIODICALLY_SHORT(17), + CLICKS_FURNI(18), + CLICKS_TILE(19), + CLICKS_USER(20), + USER_PERFORMS_ACTION(21), + CLOCK_COUNTER(22), + VARIABLE_CHANGED(23), SAY_COMMAND(0), IDLES(11), UNIDLES(11), diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredUserActionType.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredUserActionType.java new file mode 100644 index 00000000..eaf12377 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredUserActionType.java @@ -0,0 +1,18 @@ +package com.eu.habbo.habbohotel.wired; + +public final class WiredUserActionType { + public static final int WAVE = 1; + public static final int BLOW_KISS = 2; + public static final int LAUGH = 3; + public static final int AWAKE = 4; + public static final int RELAX = 5; + public static final int SIT = 6; + public static final int STAND = 7; + public static final int LAY = 8; + public static final int SIGN = 9; + public static final int DANCE = 10; + public static final int THUMB_UP = 11; + + private WiredUserActionType() { + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/WiredStack.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/WiredStack.java index 0713715a..337e0734 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/WiredStack.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/api/WiredStack.java @@ -36,9 +36,11 @@ public final class WiredStack { private final List effects; // Extra modifiers - private final boolean useOrMode; // WiredExtraOrEval present + private final int conditionEvaluationMode; // WiredExtraOrEval mode + private final int conditionEvaluationValue; // WiredExtraOrEval numeric threshold private final boolean useRandom; // WiredExtraRandom present private final boolean useUnseen; // WiredExtraUnseen present + private final boolean executeInOrder; // WiredExtraExecuteInOrder present /** * Create a new wired stack. @@ -52,7 +54,7 @@ public final class WiredStack { IWiredTrigger trigger, List conditions, List effects) { - this(triggerItem, trigger, conditions, effects, false, false, false); + this(triggerItem, trigger, conditions, effects, 0, 1, false, false, false); } /** @@ -62,24 +64,30 @@ public final class WiredStack { * @param trigger the trigger implementation * @param conditions list of conditions * @param effects list of effects - * @param useOrMode if true, conditions use OR logic (any pass = success) + * @param conditionEvaluationMode condition evaluation mode from WiredExtraOrEval + * @param conditionEvaluationValue numeric comparison value from WiredExtraOrEval * @param useRandom if true, select one random effect instead of all * @param useUnseen if true, execute effects in "unseen" order (round-robin) + * @param executeInOrder if true, execute all regular effects in stable stack order */ public WiredStack(HabboItem triggerItem, IWiredTrigger trigger, List conditions, List effects, - boolean useOrMode, + int conditionEvaluationMode, + int conditionEvaluationValue, boolean useRandom, - boolean useUnseen) { + boolean useUnseen, + boolean executeInOrder) { this.triggerItem = triggerItem; this.trigger = trigger; this.conditions = conditions != null ? Collections.unmodifiableList(conditions) : Collections.emptyList(); this.effects = effects != null ? Collections.unmodifiableList(effects) : Collections.emptyList(); - this.useOrMode = useOrMode; + this.conditionEvaluationMode = conditionEvaluationMode; + this.conditionEvaluationValue = conditionEvaluationValue; this.useRandom = useRandom; this.useUnseen = useUnseen; + this.executeInOrder = executeInOrder; } /** @@ -131,12 +139,19 @@ public final class WiredStack { } /** - * Check if OR mode is enabled (WiredExtraOrEval). - * When true, any condition passing means all pass. - * @return true if OR mode is enabled + * Get the condition evaluation mode from WiredExtraOrEval. + * @return evaluation mode code */ - public boolean useOrMode() { - return useOrMode; + public int conditionEvaluationMode() { + return conditionEvaluationMode; + } + + /** + * Get the condition evaluation numeric value from WiredExtraOrEval. + * @return comparison value + */ + public int conditionEvaluationValue() { + return conditionEvaluationValue; } /** @@ -157,6 +172,15 @@ public final class WiredStack { return useUnseen; } + /** + * Check if ordered execution mode is enabled (WiredExtraExecuteInOrder). + * When true, all regular effects execute in stable stack order. + * @return true if ordered execution is enabled + */ + public boolean executeInOrder() { + return executeInOrder; + } + /** * Get the number of conditions. * @return condition count @@ -180,9 +204,11 @@ public final class WiredStack { ", trigger=" + (trigger != null ? trigger.listensTo() : "null") + ", conditions=" + conditions.size() + ", effects=" + effects.size() + - ", orMode=" + useOrMode + + ", conditionEvaluationMode=" + conditionEvaluationMode + + ", conditionEvaluationValue=" + conditionEvaluationValue + ", random=" + useRandom + ", unseen=" + useUnseen + + ", executeInOrder=" + executeInOrder + '}'; } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java index 1002a7ce..42cbddea 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/RoomWiredStackIndex.java @@ -2,7 +2,9 @@ package com.eu.habbo.habbohotel.wired.core; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecuteInOrder; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraOrEval; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRandom; import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUnseen; @@ -156,6 +158,34 @@ public final class RoomWiredStackIndex implements WiredStackIndex { return stacks.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(stacks); } + public List getStacksAtTile(Room room, RoomTile tile) { + if (room == null || tile == null || room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + RoomSpecialTypes specialTypes = room.getRoomSpecialTypes(); + THashSet triggers = specialTypes.getTriggers(tile.x, tile.y); + + if (triggers == null || triggers.isEmpty()) { + return Collections.emptyList(); + } + + List stacks = new ArrayList<>(); + + for (InteractionWiredTrigger trigger : WiredExecutionOrderUtil.sort(triggers)) { + if (trigger == null) { + continue; + } + + WiredStack stack = buildStack(room, specialTypes, trigger, tile.x, tile.y); + if (stack != null) { + stacks.add(stack); + } + } + + return stacks.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(stacks); + } + /** * Build a single wired stack for a trigger at a specific location. */ @@ -173,18 +203,33 @@ public final class RoomWiredStackIndex implements WiredStackIndex { List effects = collectEffects(rawEffects); // Check for extras - boolean useOrMode = specialTypes.hasExtraType(x, y, WiredExtraOrEval.class); + THashSet extras = specialTypes.getExtras(x, y); + int conditionEvaluationMode = WiredExtraOrEval.MODE_ALL; + int conditionEvaluationValue = 1; boolean useRandom = specialTypes.hasExtraType(x, y, WiredExtraRandom.class); boolean useUnseen = specialTypes.hasExtraType(x, y, WiredExtraUnseen.class); + boolean executeInOrder = specialTypes.hasExtraType(x, y, WiredExtraExecuteInOrder.class); + + if (extras != null) { + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraOrEval) { + conditionEvaluationMode = ((WiredExtraOrEval) extra).getEvaluationMode(); + conditionEvaluationValue = ((WiredExtraOrEval) extra).getCompareValue(); + break; + } + } + } return new WiredStack( trigger, wrappedTrigger, conditions, effects, - useOrMode, + conditionEvaluationMode, + conditionEvaluationValue, useRandom, - useUnseen + useUnseen, + executeInOrder ); } @@ -197,7 +242,7 @@ public final class RoomWiredStackIndex implements WiredStackIndex { } List conditions = new ArrayList<>(rawConditions.size()); - for (InteractionWiredCondition condition : rawConditions) { + for (InteractionWiredCondition condition : WiredExecutionOrderUtil.sort(rawConditions)) { conditions.add(condition); } return conditions; @@ -212,7 +257,7 @@ public final class RoomWiredStackIndex implements WiredStackIndex { } List effects = new ArrayList<>(rawEffects.size()); - for (InteractionWiredEffect effect : rawEffects) { + for (InteractionWiredEffect effect : WiredExecutionOrderUtil.sort(rawEffects)) { effects.add(effect); } return effects; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredBotSourceUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredBotSourceUtil.java new file mode 100644 index 00000000..fa40b95e --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredBotSourceUtil.java @@ -0,0 +1,59 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class WiredBotSourceUtil { + public static final int SOURCE_BOT_NAME = 100; + + private WiredBotSourceUtil() { + } + + public static int normalizeBotSource(int value) { + return normalizeBotSource(value, SOURCE_BOT_NAME); + } + + public static int normalizeBotSource(int value, int fallback) { + switch (value) { + case WiredSourceUtil.SOURCE_TRIGGER: + case SOURCE_BOT_NAME: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + return value; + default: + return fallback; + } + } + + public static List resolveBots(WiredContext ctx, Room room, int botSource, String botName) { + if (ctx == null || room == null) { + return Collections.emptyList(); + } + + if (botSource == SOURCE_BOT_NAME) { + List bots = room.getBots(botName); + return (bots != null) ? new ArrayList<>(bots) : Collections.emptyList(); + } + + List resolved = new ArrayList<>(); + + for (RoomUnit roomUnit : WiredSourceUtil.resolveUsers(ctx, botSource)) { + Bot bot = room.getBot(roomUnit); + + if (bot != null && !resolved.contains(bot)) { + resolved.add(bot); + } + } + + return resolved; + } + + public static boolean requiresTriggeringUser(int botSource) { + return botSource == WiredSourceUtil.SOURCE_TRIGGER; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java index 142528e7..0faeee03 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContext.java @@ -59,6 +59,12 @@ public final class WiredContext { /** Extra settings from the trigger item (for legacy compatibility) */ private final Object[] legacySettings; + /** Runtime-local context variables shared through the current execution chain. */ + private WiredContextVariableScope contextVariables; + + /** Whether selector item resolution should include wired furniture too. */ + private boolean includeWiredSelectorItems = false; + /** * Create a new wired context. * @@ -105,6 +111,9 @@ public final class WiredContext { this.services = services; this.state = state; this.legacySettings = legacySettings; + this.contextVariables = (event.getContextVariableScope() != null) + ? event.getContextVariableScope() + : new WiredContextVariableScope(); this.targets = new WiredTargets(); // Default targets: include actor and trigger item for backwards compatibility @@ -256,6 +265,22 @@ public final class WiredContext { return legacySettings != null ? legacySettings : new Object[0]; } + public WiredContextVariableScope contextVariables() { + return this.contextVariables; + } + + public void forkContextVariables() { + this.contextVariables = this.contextVariables.copy(); + } + + public boolean includeWiredSelectorItems() { + return this.includeWiredSelectorItems; + } + + public void setIncludeWiredSelectorItems(boolean includeWiredSelectorItems) { + this.includeWiredSelectorItems = includeWiredSelectorItems; + } + // ========== Utility Methods ========== /** diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableScope.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableScope.java new file mode 100644 index 00000000..9ea07467 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableScope.java @@ -0,0 +1,134 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.Emulator; + +import java.util.LinkedHashMap; +import java.util.Map; + +public final class WiredContextVariableScope { + private final LinkedHashMap assignments; + + public WiredContextVariableScope() { + this.assignments = new LinkedHashMap<>(); + } + + private WiredContextVariableScope(Map source) { + this.assignments = new LinkedHashMap<>(); + + if (source == null || source.isEmpty()) { + return; + } + + for (Map.Entry entry : source.entrySet()) { + if (entry == null || entry.getKey() == null || entry.getKey() <= 0 || entry.getValue() == null) { + continue; + } + + this.assignments.put(entry.getKey(), entry.getValue().copy()); + } + } + + public WiredContextVariableScope copy() { + return new WiredContextVariableScope(this.assignments); + } + + public boolean hasVariable(int definitionItemId) { + return definitionItemId > 0 && this.assignments.containsKey(definitionItemId); + } + + public Integer getValue(int definitionItemId) { + VariableAssignment assignment = this.assignments.get(definitionItemId); + return assignment != null ? assignment.getValue() : null; + } + + public int getCreatedAt(int definitionItemId) { + VariableAssignment assignment = this.assignments.get(definitionItemId); + return assignment != null ? assignment.getCreatedAt() : 0; + } + + public int getUpdatedAt(int definitionItemId) { + VariableAssignment assignment = this.assignments.get(definitionItemId); + return assignment != null ? assignment.getUpdatedAt() : 0; + } + + public boolean assignValue(int definitionItemId, Integer value, boolean overrideExisting) { + if (definitionItemId <= 0) { + return false; + } + + VariableAssignment existingAssignment = this.assignments.get(definitionItemId); + + if (existingAssignment != null && !overrideExisting) { + return false; + } + + int now = Emulator.getIntUnixTimestamp(); + + if (existingAssignment == null || overrideExisting) { + this.assignments.put(definitionItemId, new VariableAssignment(value, now, now)); + return true; + } + + return false; + } + + public boolean updateValue(int definitionItemId, Integer value) { + if (definitionItemId <= 0) { + return false; + } + + VariableAssignment assignment = this.assignments.get(definitionItemId); + if (assignment == null) { + return false; + } + + if ((assignment.getValue() == null && value == null) + || (assignment.getValue() != null && assignment.getValue().equals(value))) { + return false; + } + + assignment.setValue(value, Emulator.getIntUnixTimestamp()); + return true; + } + + public boolean removeValue(int definitionItemId) { + if (definitionItemId <= 0) { + return false; + } + + return this.assignments.remove(definitionItemId) != null; + } + + public static final class VariableAssignment { + private Integer value; + private final int createdAt; + private int updatedAt; + + public VariableAssignment(Integer value, int createdAt, int updatedAt) { + this.value = value; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Integer getValue() { + return this.value; + } + + public int getCreatedAt() { + return this.createdAt; + } + + public int getUpdatedAt() { + return this.updatedAt; + } + + public void setValue(Integer value, int updatedAt) { + this.value = value; + this.updatedAt = updatedAt; + } + + private VariableAssignment copy() { + return new VariableAssignment(this.value, this.createdAt, this.updatedAt); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableSupport.java new file mode 100644 index 00000000..e1e45791 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredContextVariableSupport.java @@ -0,0 +1,152 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraContextVariable; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.messages.outgoing.wired.WiredUserVariablesDataComposer; +import gnu.trove.set.hash.THashSet; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public final class WiredContextVariableSupport { + private WiredContextVariableSupport() { + } + + public static List getDefinitions(Room room) { + List definitions = new ArrayList<>(); + + if (room == null || room.getRoomSpecialTypes() == null) { + return definitions; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(); + if (extras == null || extras.isEmpty()) { + return definitions; + } + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraContextVariable) { + definitions.add((WiredExtraContextVariable) extra); + } + } + + definitions.sort(Comparator + .comparing(WiredExtraContextVariable::getVariableName, String.CASE_INSENSITIVE_ORDER) + .thenComparingInt(WiredExtraContextVariable::getId)); + + return definitions; + } + + public static List createDefinitionInfos(Room room) { + List definitions = new ArrayList<>(); + + for (WiredExtraContextVariable definition : getDefinitions(room)) { + if (definition == null || definition.getVariableName() == null || definition.getVariableName().isEmpty()) { + continue; + } + + definitions.add(new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + 0, + WiredVariableTextConnectorSupport.isTextConnected(room, definition.getId()), + false)); + } + + return definitions; + } + + public static WiredExtraContextVariable getDefinition(Room room, int definitionItemId) { + if (room == null || room.getRoomSpecialTypes() == null || definitionItemId <= 0) { + return null; + } + + InteractionWiredExtra extra = room.getRoomSpecialTypes().getExtra(definitionItemId); + return (extra instanceof WiredExtraContextVariable) ? (WiredExtraContextVariable) extra : null; + } + + public static WiredVariableDefinitionInfo getDefinitionInfo(Room room, int definitionItemId) { + WiredExtraContextVariable definition = getDefinition(room, definitionItemId); + if (definition == null || definition.getVariableName() == null || definition.getVariableName().trim().isEmpty()) { + return null; + } + + return new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + 0, + WiredVariableTextConnectorSupport.isTextConnected(room, definition.getId()), + false); + } + + public static boolean hasDefinition(Room room, int definitionItemId) { + return getDefinitionInfo(room, definitionItemId) != null; + } + + public static boolean assignVariable(WiredContext ctx, Room room, int definitionItemId, Integer value, boolean overrideExisting) { + WiredExtraContextVariable definition = getDefinition(room, definitionItemId); + if (ctx == null || definition == null) { + return false; + } + + if (overrideExisting && ctx.contextVariables().hasVariable(definitionItemId)) { + ctx.forkContextVariables(); + } + + return ctx.contextVariables().assignValue(definitionItemId, definition.hasValue() ? value : null, overrideExisting); + } + + public static boolean updateVariableValue(WiredContext ctx, Room room, int definitionItemId, Integer value) { + WiredExtraContextVariable definition = getDefinition(room, definitionItemId); + if (ctx == null || definition == null || !definition.hasValue()) { + return false; + } + + return ctx.contextVariables().updateValue(definitionItemId, value); + } + + public static boolean removeVariable(WiredContext ctx, Room room, int definitionItemId) { + return ctx != null && getDefinition(room, definitionItemId) != null && ctx.contextVariables().removeValue(definitionItemId); + } + + public static boolean hasVariable(WiredContext ctx, int definitionItemId) { + return ctx != null && ctx.contextVariables().hasVariable(definitionItemId); + } + + public static Integer getCurrentValue(WiredContext ctx, int definitionItemId) { + return ctx != null ? ctx.contextVariables().getValue(definitionItemId) : null; + } + + public static int getCreatedAt(WiredContext ctx, int definitionItemId) { + return ctx != null ? ctx.contextVariables().getCreatedAt(definitionItemId) : 0; + } + + public static int getUpdatedAt(WiredContext ctx, int definitionItemId) { + return ctx != null ? ctx.contextVariables().getUpdatedAt(definitionItemId) : 0; + } + + public static void broadcastDefinitions(Room room) { + if (room == null) { + return; + } + + WiredUserVariablesDataComposer composer = new WiredUserVariablesDataComposer( + room.getUserVariableManager().createSnapshot(), + room.getFurniVariableManager().createSnapshot(), + room.getRoomVariableManager().createSnapshot()); + + room.getHabbos().forEach(habbo -> + { + if (habbo == null || habbo.getClient() == null) { + return; + } + + habbo.getClient().sendResponse(composer); + }); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java index 28300ca6..91f25afe 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java @@ -4,14 +4,26 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurni; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurniByVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUser; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUsersByVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecutionLimit; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraOrEval; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraRandom; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUnseen; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerHabboClicksUser; +import com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerHabboSaysKeyword; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredConditionOperator; +import com.eu.habbo.habbohotel.wired.WiredEffectType; import com.eu.habbo.habbohotel.wired.api.IWiredCondition; import com.eu.habbo.habbohotel.wired.api.IWiredEffect; import com.eu.habbo.habbohotel.wired.api.WiredStack; +import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer; import com.eu.habbo.plugin.events.furniture.wired.WiredStackExecutedEvent; @@ -31,7 +43,7 @@ import java.util.concurrent.ConcurrentHashMap; * It receives {@link WiredEvent} objects, finds matching stacks via {@link WiredStackIndex}, * evaluates conditions, and executes effects. *

- * + * *

Execution Flow:

*
    *
  1. Receive event via {@link #handleEvent(WiredEvent)}
  2. @@ -41,14 +53,14 @@ import java.util.concurrent.ConcurrentHashMap; *
  3. Execute effects (respecting random/unseen modifiers)
  4. *
  5. Handle delays for timed effects
  6. *
- * + * *

Safety Features:

*
    *
  • Step limits via {@link WiredState} prevent infinite loops
  • *
  • Effect cooldowns prevent rapid re-triggering
  • *
  • Exceptions are caught and logged, not propagated
  • *
- * + * * @see WiredEvent * @see WiredContext * @see WiredStackIndex @@ -56,38 +68,71 @@ import java.util.concurrent.ConcurrentHashMap; public final class WiredEngine { private static final Logger LOGGER = LoggerFactory.getLogger(WiredEngine.class); - + /** Maximum recursion depth to prevent infinite loops (e.g., collision + chase) */ public static int MAX_RECURSION_DEPTH = 10; - + /** Maximum events of same type per room within rate limit window before banning */ public static int MAX_EVENTS_PER_WINDOW = 100; - + /** Time window for counting rapid events (milliseconds) */ public static long RATE_LIMIT_WINDOW_MS = 10000; - + /** Duration to ban wired execution in a room after abuse detected (milliseconds) */ public static long WIRED_BAN_DURATION_MS = 600000; + /** Monitor usage window in milliseconds */ + public static int MONITOR_USAGE_WINDOW_MS = 1000; + + /** Monitor execution cap per room window */ + public static int MONITOR_USAGE_LIMIT = 1000; + + /** Maximum delayed events allowed per room at the same time */ + public static int MONITOR_DELAYED_EVENTS_LIMIT = 100; + + /** Average execution threshold that marks overload */ + public static int MONITOR_OVERLOAD_AVERAGE_MS = 50; + + /** Peak execution threshold that marks overload */ + public static int MONITOR_OVERLOAD_PEAK_MS = 150; + + /** Consecutive overloaded windows required before recording overload */ + public static int MONITOR_OVERLOAD_CONSECUTIVE_WINDOWS = 2; + + /** Usage percentage threshold that marks a room as heavy */ + public static int MONITOR_HEAVY_USAGE_PERCENT = 70; + + /** Consecutive windows above threshold before marking heavy */ + public static int MONITOR_HEAVY_CONSECUTIVE_WINDOWS = 5; + + /** Delayed queue percentage threshold that contributes to heavy state */ + public static int MONITOR_HEAVY_DELAYED_PERCENT = 60; + private final WiredServices services; private final WiredStackIndex index; private final int maxStepsPerStack; - + /** Track unseen effect indices per room+tile for round-robin selection */ private final ConcurrentHashMap unseenIndices; - + /** Track recursion depth per room to prevent infinite loops */ private final ConcurrentHashMap roomRecursionDepth; - + /** Track event timestamps per room+eventType for rate limiting: key = "roomId:eventType" */ private final ConcurrentHashMap eventRateLimiters; - + /** Track rooms that are banned from wired execution: roomId -> ban expiry timestamp */ private final ConcurrentHashMap bannedRooms; + /** Track monitor diagnostics per room */ + private final ConcurrentHashMap roomDiagnostics; + + /** Cache room+eventType+sourceItemId -> matching stacks for source-triggered timer events */ + private final ConcurrentHashMap> sourceStacksByTriggerKey; + /** * Create a new wired engine. - * + * * @param services the services for performing side effects * @param index the stack index for finding matching stacks * @param maxStepsPerStack maximum steps per stack execution (loop protection) @@ -96,7 +141,7 @@ public final class WiredEngine { if (services == null) throw new IllegalArgumentException("Services cannot be null"); if (index == null) throw new IllegalArgumentException("Index cannot be null"); if (maxStepsPerStack <= 0) throw new IllegalArgumentException("Max steps must be positive"); - + this.services = services; this.index = index; this.maxStepsPerStack = maxStepsPerStack; @@ -104,15 +149,21 @@ public final class WiredEngine { this.roomRecursionDepth = new ConcurrentHashMap<>(); this.eventRateLimiters = new ConcurrentHashMap<>(); this.bannedRooms = new ConcurrentHashMap<>(); + this.roomDiagnostics = new ConcurrentHashMap<>(); + this.sourceStacksByTriggerKey = new ConcurrentHashMap<>(); } /** * Handle a wired event by finding and executing matching stacks. - * + * * @param event the event to handle * @return true if any stack was triggered (useful for SAY_SOMETHING to suppress message) */ public boolean handleEvent(WiredEvent event) { + return handleEvent(event, false); + } + + public boolean handleEvent(WiredEvent event, boolean negateConditions) { if (event == null) { return false; } @@ -121,32 +172,32 @@ public final class WiredEngine { if (room == null || !room.isLoaded()) { return false; } - + int roomId = room.getId(); - - // Check if room is banned from wired execution - if (isRoomBanned(roomId)) { - return false; - } - - // Check rate limiting to prevent rapid-fire event spam (e.g., collision + chase loop) + + // Soft rate limiting to prevent rapid-fire event spam without banning whole rooms if (isRateLimited(roomId, room, event.getType())) { - // Room has been banned, all events will be dropped return false; } - + // Check and increment recursion depth to prevent infinite loops int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0); if (currentDepth >= MAX_RECURSION_DEPTH) { + getDiagnostics(roomId).recordRecursionTimeout( + System.currentTimeMillis(), + String.format("Recursion depth %d/%d while handling %s", currentDepth, MAX_RECURSION_DEPTH, event.getType().name()), + event.getType().name(), + 0 + ); LOGGER.warn("Wired recursion limit reached in room {} (depth: {}). " + "Possible infinite loop detected (e.g., collision + chase). Aborting.", roomId, currentDepth); debug(room, "RECURSION LIMIT REACHED - aborting to prevent crash"); return false; } roomRecursionDepth.put(roomId, currentDepth + 1); - + try { - return handleEventInternal(event, room); + return handleEventInternal(event, room, negateConditions); } finally { // Decrement recursion depth int newDepth = roomRecursionDepth.getOrDefault(roomId, 1) - 1; @@ -157,11 +208,133 @@ public final class WiredEngine { } } } - + + /** + * Handle a wired event when the source trigger item is already known. + * This is mainly used by timed wired triggers to avoid scanning unrelated stacks. + * + * @param event the event to handle + * @param sourceItemId the trigger item id that originated the event + * @return true if any matching stack was triggered + */ + public boolean handleEventForSourceItem(WiredEvent event, int sourceItemId) { + if (event == null || sourceItemId <= 0) { + return false; + } + + Room room = event.getRoom(); + if (room == null || !room.isLoaded()) { + return false; + } + + int roomId = room.getId(); + + if (isRateLimited(roomId, room, event.getType())) { + return false; + } + + int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0); + if (currentDepth >= MAX_RECURSION_DEPTH) { + LOGGER.warn("Wired recursion limit reached in room {} (depth: {}). " + + "Possible infinite loop detected (source item execution). Aborting.", roomId, currentDepth); + debug(room, "RECURSION LIMIT REACHED - aborting source-item execution"); + return false; + } + roomRecursionDepth.put(roomId, currentDepth + 1); + + try { + return handleEventForSourceItemInternal(event, room, sourceItemId); + } finally { + int newDepth = roomRecursionDepth.getOrDefault(roomId, 1) - 1; + if (newDepth <= 0) { + roomRecursionDepth.remove(roomId); + } else { + roomRecursionDepth.put(roomId, newDepth); + } + } + } + + /** + * Internal event handling optimized for a known source trigger item. + */ + private boolean handleEventForSourceItemInternal(WiredEvent event, Room room, int sourceItemId) { + List stacks = getStacksForSourceItem(room, event.getType(), sourceItemId); + if (stacks.isEmpty()) { + return false; + } + + debug(room, "Processing {} stacks for event type {} from source item {}", stacks.size(), event.getType(), sourceItemId); + + boolean anyTriggered = false; + boolean suppressSaysOutput = false; + long triggerTime = event.getCreatedAtMs(); + + for (WiredStack stack : stacks) { + try { + boolean triggered = processStack(stack, event, triggerTime); + if (triggered) { + anyTriggered = true; + + if ((event.getType() == WiredEvent.Type.USER_SAYS) + && (stack.triggerItem() instanceof WiredTriggerHabboSaysKeyword) + && ((WiredTriggerHabboSaysKeyword) stack.triggerItem()).isHideMessage()) { + suppressSaysOutput = true; + } + } + } catch (WiredLimitException limitEx) { + debug(room, "Stack execution stopped (limit): {}", limitEx.getMessage()); + } catch (Exception ex) { + LOGGER.error("Error processing source wired stack in room {} for item {}: {}", + room.getId(), sourceItemId, ex.getMessage(), ex); + debug(room, "Source stack error: {}", ex.getMessage()); + } + } + + if (event.getType() == WiredEvent.Type.USER_SAYS) { + return suppressSaysOutput; + } + + return anyTriggered; + } + + /** + * Find all stacks for a specific room/event/source item combination. + * Multiple stacks can legally share the same trigger item. + */ + private List getStacksForSourceItem(Room room, WiredEvent.Type eventType, int sourceItemId) { + String cacheKey = room.getId() + ":" + eventType.name() + ":" + sourceItemId; + + List cached = sourceStacksByTriggerKey.get(cacheKey); + if (cached != null) { + return cached; + } + + List allStacks = index.getStacks(room, eventType); + if (allStacks.isEmpty()) { + sourceStacksByTriggerKey.put(cacheKey, Collections.emptyList()); + return Collections.emptyList(); + } + + List matching = new ArrayList<>(); + for (WiredStack stack : allStacks) { + if (stack == null || stack.triggerItem() == null) { + continue; + } + + if (stack.triggerItem().getId() == sourceItemId) { + matching.add(stack); + } + } + + List result = matching.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(matching); + sourceStacksByTriggerKey.put(cacheKey, result); + return result; + } + /** * Internal event handling after recursion check. */ - private boolean handleEventInternal(WiredEvent event, Room room) { + private boolean handleEventInternal(WiredEvent event, Room room, boolean negateConditions) { // Find candidate stacks for this event type List stacks = index.getStacks(room, event.getType()); @@ -172,13 +345,20 @@ public final class WiredEngine { debug(room, "Processing {} stacks for event type {}", stacks.size(), event.getType()); boolean anyTriggered = false; - long currentTime = System.currentTimeMillis(); + boolean suppressSaysOutput = false; + long triggerTime = event.getCreatedAtMs(); for (WiredStack stack : stacks) { try { - boolean triggered = processStack(stack, event, currentTime); + boolean triggered = processStack(stack, event, triggerTime, negateConditions); if (triggered) { anyTriggered = true; + + if ((event.getType() == WiredEvent.Type.USER_SAYS) + && (stack.triggerItem() instanceof WiredTriggerHabboSaysKeyword) + && ((WiredTriggerHabboSaysKeyword) stack.triggerItem()).isHideMessage()) { + suppressSaysOutput = true; + } } } catch (WiredLimitException limitEx) { debug(room, "Stack execution stopped (limit): {}", limitEx.getMessage()); @@ -188,6 +368,10 @@ public final class WiredEngine { } } + if (event.getType() == WiredEvent.Type.USER_SAYS) { + return suppressSaysOutput; + } + return anyTriggered; } @@ -195,10 +379,15 @@ public final class WiredEngine { * Process a single wired stack. */ private boolean processStack(WiredStack stack, WiredEvent event, long currentTime) { + return processStack(stack, event, currentTime, false); + } + + private boolean processStack(WiredStack stack, WiredEvent event, long currentTime, boolean negateConditions) { Room room = event.getRoom(); + WiredTextInputCaptureSupport.CaptureResult captureResult = resolveTextInputCapture(stack, event); // Check if trigger matches - if (!stack.trigger().matches(stack.triggerItem(), event)) { + if (!captureResult.matches()) { return false; } @@ -207,44 +396,51 @@ public final class WiredEngine { return false; } + if (!stackHasExecutableOutcome(stack, event)) { + return false; + } + // Create execution context with stack reference WiredState state = new WiredState(maxStepsPerStack); WiredContext ctx = new WiredContext(event, stack.triggerItem(), stack, services, state, null); + WiredTextInputCaptureSupport.applyToContext(ctx, room, captureResult); + WiredRoomDiagnostics diagnostics = getDiagnostics(room.getId()); // Initial step for trigger state.step(); - - // Activate the trigger box animation - if (stack.triggerItem() instanceof InteractionWiredTrigger) { - InteractionWiredTrigger trigger = (InteractionWiredTrigger) stack.triggerItem(); - trigger.activateBox(room, event.getActor().orElse(null), currentTime); - } - debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})", - event.getType(), + int stackCost = estimateStackCost(stack, roomRecursionDepth.getOrDefault(room.getId(), 0)); + String monitorSourceLabel = getMonitorSourceLabel(stack.triggerItem(), event); + int monitorSourceId = getMonitorSourceId(stack.triggerItem()); + + debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})", + event.getType(), stack.triggerItem() != null ? stack.triggerItem().getId() : "null", stack.conditions().size(), stack.effects().size()); - - // Activate extras (for their animation) - activateExtras(room, stack.triggerItem(), event.getActor().orElse(null), currentTime); // Run selectors before conditions so targets are available + List executedSelectors = Collections.emptyList(); if (stack.hasEffects()) { - executeSelectors(stack, ctx, currentTime); + executedSelectors = executeSelectors(stack, ctx); + applySelectionFilterExtras(stack, ctx, executedSelectors); } - // Evaluate conditions - if (stack.hasConditions()) { - debug(room, "Evaluating {} conditions...", stack.conditions().size()); - boolean conditionsPassed = evaluateConditions(stack, ctx); - debug(room, "Conditions result: {}", conditionsPassed ? "PASSED" : "FAILED"); - if (!conditionsPassed) { - debug(room, "Conditions failed, aborting stack"); - return false; - } - } else { - debug(room, "No conditions in stack, proceeding to effects"); + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); + List executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); + boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event); + + if (!shouldContinueAfterConditionCheck(stack, room, conditionsPassedForExecution, executableEffects, hasSpecialOutcome)) { + return false; + } + + WiredExtraExecutionLimit executionLimitExtra = getExecutionLimitExtra(room, stack); + if (executionLimitExtra != null && !executionLimitExtra.tryAcquireExecutionSlot(currentTime)) { + debug(room, "Execution limit blocked stack {} (max {} in {} ms)", + stack.triggerItem() != null ? stack.triggerItem().getId() : "null", + executionLimitExtra.getMaxExecutions(), + executionLimitExtra.getTimeWindowMs()); + return false; } // Fire plugin event (WiredStackTriggeredEvent) @@ -253,166 +449,556 @@ public final class WiredEngine { return false; } + if (!diagnostics.tryConsumeExecutionBudget( + stackCost, + currentTime, + monitorSourceLabel, + monitorSourceId, + buildStackMonitorReason(stack, event, stackCost))) { + debug(room, "Execution cap blocked stack {}", stack.triggerItem() != null ? stack.triggerItem().getId() : "null"); + return false; + } + + if (conditionsPassedForExecution + && (event.getType() == WiredEvent.Type.USER_CLICKS_USER) + && (stack.triggerItem() instanceof WiredTriggerHabboClicksUser) + && event.getActor().isPresent()) { + WiredTriggerHabboClicksUser clickUserTrigger = (WiredTriggerHabboClicksUser) stack.triggerItem(); + WiredTriggerHabboClicksUser.applyRuntimeOptions( + event.getActor().get(), + clickUserTrigger.isBlockMenuOpen(), + clickUserTrigger.isDoNotRotate()); + } + + RoomUnit actor = event.getActor().orElse(null); + + // Only show the trigger/selector activation when the stack is actually allowed to continue. + if (stack.triggerItem() instanceof InteractionWiredTrigger) { + InteractionWiredTrigger trigger = (InteractionWiredTrigger) stack.triggerItem(); + trigger.activateBox(room, actor, currentTime); + } + + activateExtras(room, stack.triggerItem(), actor, currentTime); + finalizeSelectors(executedSelectors, ctx, currentTime); + // Execute effects - if (stack.hasEffects()) { - executeEffects(stack, ctx, currentTime); + if (!executableEffects.isEmpty()) { + executeEffects(stack, executableEffects, ctx, currentTime); } // Fire executed event fireExecutedEvent(stack, event); + diagnostics.recordExecution( + state.elapsedMs(), + System.currentTimeMillis(), + monitorSourceLabel, + monitorSourceId, + buildExecutionMonitorReason(stack, state.elapsedMs()) + ); return true; } + public boolean executeDirectStack(WiredStack stack, WiredEvent event, boolean negateConditions) { + if (stack == null || event == null) { + return false; + } + + Room room = event.getRoom(); + if (room == null) { + return false; + } + + if (stack.trigger().requiresActor() && !event.getActor().isPresent()) { + return false; + } + + if (!stackHasExecutableOutcome(stack, event)) { + return false; + } + + long currentTime = System.currentTimeMillis(); + + WiredState state = new WiredState(maxStepsPerStack); + WiredContext ctx = new WiredContext(event, stack.triggerItem(), stack, services, state, null); + WiredRoomDiagnostics diagnostics = getDiagnostics(room.getId()); + + state.step(); + + int stackCost = estimateStackCost(stack, roomRecursionDepth.getOrDefault(room.getId(), 0)); + String monitorSourceLabel = getMonitorSourceLabel(stack.triggerItem(), event); + int monitorSourceId = getMonitorSourceId(stack.triggerItem()); + + debug(room, "Direct stack execution for item {} (conditions: {}, effects: {}, negated: {})", + stack.triggerItem() != null ? stack.triggerItem().getId() : "null", + stack.conditions().size(), + stack.effects().size(), + negateConditions); + + List executedSelectors = Collections.emptyList(); + if (stack.hasEffects()) { + executedSelectors = executeSelectors(stack, ctx); + applySelectionFilterExtras(stack, ctx, executedSelectors); + } + + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); + List executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); + boolean hasSpecialOutcome = conditionsPassedForExecution && hasSpecialTriggerOutcome(stack, event); + + if (!shouldContinueAfterConditionCheck(stack, room, conditionsPassedForExecution, executableEffects, hasSpecialOutcome)) { + return false; + } + + WiredExtraExecutionLimit executionLimitExtra = getExecutionLimitExtra(room, stack); + if (executionLimitExtra != null && !executionLimitExtra.tryAcquireExecutionSlot(currentTime)) { + debug(room, "Execution limit blocked direct stack {} (max {} in {} ms)", + stack.triggerItem() != null ? stack.triggerItem().getId() : "null", + executionLimitExtra.getMaxExecutions(), + executionLimitExtra.getTimeWindowMs()); + return false; + } + + if (!fireTriggeredEvent(stack, event)) { + debug(room, "Direct stack cancelled by plugin"); + return false; + } + + if (!diagnostics.tryConsumeExecutionBudget( + stackCost, + currentTime, + monitorSourceLabel, + monitorSourceId, + buildStackMonitorReason(stack, event, stackCost))) { + debug(room, "Execution cap blocked direct stack {}", stack.triggerItem() != null ? stack.triggerItem().getId() : "null"); + return false; + } + + RoomUnit actor = event.getActor().orElse(null); + + if (stack.triggerItem() instanceof InteractionWiredTrigger) { + InteractionWiredTrigger trigger = (InteractionWiredTrigger) stack.triggerItem(); + trigger.activateBox(room, actor, currentTime); + } + + activateExtras(room, stack.triggerItem(), actor, currentTime); + finalizeSelectors(executedSelectors, ctx, currentTime); + + if (!executableEffects.isEmpty()) { + executeEffects(stack, executableEffects, ctx, currentTime); + } + + fireExecutedEvent(stack, event); + diagnostics.recordExecution( + state.elapsedMs(), + System.currentTimeMillis(), + monitorSourceLabel, + monitorSourceId, + buildExecutionMonitorReason(stack, state.elapsedMs()) + ); + + return true; + } + + public boolean shouldExecuteDirectStack(WiredStack stack, WiredEvent event, boolean negateConditions) { + if (stack == null || event == null) { + return false; + } + + Room room = event.getRoom(); + if (room == null) { + return false; + } + + if (stack.trigger().requiresActor() && !event.getActor().isPresent()) { + return false; + } + + if (!stack.hasEffects()) { + return false; + } + + WiredState state = new WiredState(maxStepsPerStack); + WiredContext ctx = new WiredContext(event, stack.triggerItem(), stack, services, state, null); + state.step(); + + List executedSelectors = Collections.emptyList(); + if (stack.hasEffects()) { + executedSelectors = executeSelectors(stack, ctx); + applySelectionFilterExtras(stack, ctx, executedSelectors); + } + + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, negateConditions); + List executableEffects = getExecutableEffectsForCurrentExecution(stack, conditionsPassedForExecution); + return !executableEffects.isEmpty(); + } + + private boolean wouldTriggerStack(WiredStack stack, WiredEvent event, long currentTime) { + Room room = event.getRoom(); + WiredTextInputCaptureSupport.CaptureResult captureResult = resolveTextInputCapture(stack, event); + + if (!captureResult.matches()) { + return false; + } + + if (stack.trigger().requiresActor() && !event.getActor().isPresent()) { + return false; + } + + if (!stackHasExecutableOutcome(stack, event)) { + return false; + } + + WiredState state = new WiredState(maxStepsPerStack); + WiredContext ctx = new WiredContext(event, stack.triggerItem(), stack, services, state, null); + WiredTextInputCaptureSupport.applyToContext(ctx, room, captureResult); + + state.step(); + + List executedSelectors = Collections.emptyList(); + if (stack.hasEffects()) { + executedSelectors = executeSelectors(stack, ctx); + applySelectionFilterExtras(stack, ctx, executedSelectors); + } + + boolean conditionsPassedForExecution = getConditionOutcomeForExecution(stack, ctx, false); + if (!conditionsPassedForExecution) { + return false; + } + + List executableEffects = getExecutableEffectsForCurrentExecution(stack, true); + boolean hasSpecialOutcome = hasSpecialTriggerOutcome(stack, event); + if (executableEffects.isEmpty() && !hasSpecialOutcome) { + return false; + } + + WiredExtraExecutionLimit executionLimitExtra = getExecutionLimitExtra(room, stack); + return executionLimitExtra == null || executionLimitExtra.canExecuteAt(currentTime); + } + + private boolean stackHasExecutableOutcome(WiredStack stack, WiredEvent event) { + if (stack == null) { + return false; + } + + if (stack.hasEffects()) { + return true; + } + + if (stack.triggerItem() instanceof WiredTriggerHabboSaysKeyword) { + return ((WiredTriggerHabboSaysKeyword) stack.triggerItem()).isHideMessage(); + } + + if ((event != null) + && (event.getType() == WiredEvent.Type.USER_CLICKS_USER) + && (stack.triggerItem() instanceof WiredTriggerHabboClicksUser)) { + WiredTriggerHabboClicksUser trigger = (WiredTriggerHabboClicksUser) stack.triggerItem(); + return trigger.isBlockMenuOpen() || trigger.isDoNotRotate(); + } + + return false; + } + + private boolean hasSpecialTriggerOutcome(WiredStack stack, WiredEvent event) { + if (stack == null) { + return false; + } + + if (stack.triggerItem() instanceof WiredTriggerHabboSaysKeyword) { + return ((WiredTriggerHabboSaysKeyword) stack.triggerItem()).isHideMessage(); + } + + if ((event != null) + && (event.getType() == WiredEvent.Type.USER_CLICKS_USER) + && (stack.triggerItem() instanceof WiredTriggerHabboClicksUser)) { + WiredTriggerHabboClicksUser trigger = (WiredTriggerHabboClicksUser) stack.triggerItem(); + return trigger.isBlockMenuOpen() || trigger.isDoNotRotate(); + } + + return false; + } + + private boolean getConditionOutcomeForExecution(WiredStack stack, WiredContext ctx, boolean negateConditions) { + if (!stack.hasConditions()) { + return !negateConditions; + } + + return shouldConditionsPass(stack, ctx, negateConditions); + } + + private List getExecutableEffectsForCurrentExecution(WiredStack stack, boolean conditionsPassed) { + List executableEffects = new ArrayList<>(); + + for (IWiredEffect effect : stack.effects()) { + if (effect == null || effect.isSelector()) { + continue; + } + + boolean negativeEffect = isNegativeConditionEffect(effect); + + if (conditionsPassed) { + if (!negativeEffect) { + executableEffects.add(effect); + } + continue; + } + + if (stack.hasConditions() && negativeEffect) { + executableEffects.add(effect); + } + } + + return executableEffects; + } + + private boolean isNegativeConditionEffect(IWiredEffect effect) { + if (!(effect instanceof InteractionWiredEffect)) { + return false; + } + + WiredEffectType effectType = ((InteractionWiredEffect) effect).getType(); + return effectType == WiredEffectType.NEG_CALL_STACKS || effectType == WiredEffectType.NEG_SEND_SIGNAL; + } + + private WiredTextInputCaptureSupport.CaptureResult resolveTextInputCapture(WiredStack stack, WiredEvent event) { + if (stack == null || event == null) { + return WiredTextInputCaptureSupport.CaptureResult.noMatch(); + } + + if (event.getType() != WiredEvent.Type.USER_SAYS || !(stack.triggerItem() instanceof WiredTriggerHabboSaysKeyword)) { + return stack.trigger().matches(stack.triggerItem(), event) + ? WiredTextInputCaptureSupport.CaptureResult.matched(new LinkedHashMap<>()) + : WiredTextInputCaptureSupport.CaptureResult.noMatch(); + } + + return WiredTextInputCaptureSupport.resolve(stack, event); + } + /** * Evaluate all conditions in a stack. */ private boolean evaluateConditions(WiredStack stack, WiredContext ctx) { List conditions = stack.conditions(); - - if (stack.useOrMode()) { - // OR mode: at least one condition must pass - return evaluateOrMode(conditions, ctx); - } else { - // Standard mode: use individual operators - return evaluateStandardMode(conditions, ctx); - } + + return evaluateConditionsByMode(conditions, ctx, stack.conditionEvaluationMode(), stack.conditionEvaluationValue()); } - /** - * Evaluate conditions in OR mode (any pass = success). - */ - private boolean evaluateOrMode(List conditions, WiredContext ctx) { - // Group by condition type (for legacy compatibility) - Map typeResults = new HashMap<>(); - - for (IWiredCondition condition : conditions) { - ctx.state().step(); - - String typeName = condition.getClass().getSimpleName(); - if (!typeResults.containsKey(typeName) && condition.evaluate(ctx)) { - typeResults.put(typeName, true); - } - } - - // At least one condition type must have passed - return !typeResults.isEmpty(); - } + private boolean shouldContinueAfterConditionCheck(WiredStack stack, Room room, boolean conditionsPassedForExecution, List executableEffects, boolean hasSpecialOutcome) { + if (stack.hasConditions()) { + debug(room, "Evaluating {} conditions...", stack.conditions().size()); - /** - * Evaluate conditions in standard mode using operators. - */ - private boolean evaluateStandardMode(List conditions, WiredContext ctx) { - Room room = ctx.room(); - - // First pass: collect all OR conditions that passed - Map orResults = new HashMap<>(); - for (IWiredCondition condition : conditions) { - if (condition.operator() == WiredConditionOperator.OR) { - ctx.state().step(); - String typeName = condition.getClass().getSimpleName(); - boolean result = condition.evaluate(ctx); - debug(room, " Condition (OR) {}: {}", typeName, result ? "PASS" : "FAIL"); - if (!orResults.containsKey(typeName) && result) { - orResults.put(typeName, true); - } + if (!conditionsPassedForExecution && !executableEffects.isEmpty()) { + debug(room, "Conditions failed, executing negative effects"); + return true; } - } - // Second pass: verify all conditions - for (IWiredCondition condition : conditions) { - boolean passes; - String typeName = condition.getClass().getSimpleName(); - - if (condition.operator() == WiredConditionOperator.OR) { - // OR: passes if any of same type passed - passes = orResults.containsKey(typeName); - debug(room, " Condition (OR check) {}: {}", typeName, passes ? "PASS" : "FAIL"); - } else { - // AND: must evaluate and pass - ctx.state().step(); - passes = condition.evaluate(ctx); - debug(room, " Condition (AND) {}: {}", typeName, passes ? "PASS" : "FAIL"); - } - - if (!passes) { + if (!conditionsPassedForExecution) { + debug(room, "Conditions failed, aborting stack"); return false; } + + if (hasSpecialOutcome || !executableEffects.isEmpty()) { + return true; + } + + debug(room, "Conditions passed, but no executable effects remain"); + return false; } - - return true; + + if (!conditionsPassedForExecution) { + debug(room, "No conditions in stack, negated execution aborted"); + return false; + } + + if (hasSpecialOutcome || !executableEffects.isEmpty()) { + debug(room, "No conditions in stack, proceeding to effects"); + return true; + } + + debug(room, "No conditions in stack, but no executable effects remain"); + return false; + } + + private boolean shouldConditionsPass(WiredStack stack, WiredContext ctx, boolean negateConditions) { + boolean conditionsPassed = evaluateConditions(stack, ctx); + debug(ctx.room(), "Conditions result: {}", conditionsPassed ? "PASSED" : "FAILED"); + return negateConditions ? !conditionsPassed : conditionsPassed; + } + + /** + * Evaluate conditions according to the configured stack mode. + */ + private boolean evaluateConditionsByMode(List conditions, WiredContext ctx, int evaluationMode, int evaluationValue) { + if (conditions == null || conditions.isEmpty()) { + return true; + } + + Room room = ctx.room(); + Map> groupedOrResults = new LinkedHashMap<>(); + int matchedRequirements = 0; + int totalRequirements = 0; + + for (IWiredCondition condition : conditions) { + ctx.state().step(); + + boolean result = condition.evaluate(ctx); + String conditionKey = getConditionGroupKey(condition); + + if (condition.operator() == WiredConditionOperator.OR) { + groupedOrResults.computeIfAbsent(conditionKey, ignored -> new ArrayList<>()).add(result); + debug(room, " Condition (OR group {}) {}: {}", conditionKey, condition.getClass().getSimpleName(), result ? "PASS" : "FAIL"); + continue; + } + + totalRequirements++; + + if (result) { + matchedRequirements++; + } + + debug(room, " Condition {}: {}", condition.getClass().getSimpleName(), result ? "PASS" : "FAIL"); + } + + for (Map.Entry> entry : groupedOrResults.entrySet()) { + totalRequirements++; + + boolean groupPassed = entry.getValue().stream().anyMatch(Boolean::booleanValue); + if (groupPassed) { + matchedRequirements++; + } + + debug(room, " Condition (OR result {}) : {}", entry.getKey(), groupPassed ? "PASS" : "FAIL"); + } + + boolean matches = WiredExtraOrEval.matchesMode(evaluationMode, matchedRequirements, totalRequirements, evaluationValue); + + debug(room, "Condition eval mode {} value {} matched {}/{} logical requirements => {}", evaluationMode, evaluationValue, matchedRequirements, totalRequirements, matches ? "PASS" : "FAIL"); + return matches; + } + + private String getConditionGroupKey(IWiredCondition condition) { + if (condition instanceof InteractionWiredCondition) { + return String.valueOf(((InteractionWiredCondition) condition).getType()); + } + + return condition.getClass().getName(); } /** * Execute effects in a stack. */ - private void executeEffects(WiredStack stack, WiredContext ctx, long currentTime) { - List effects = stack.effects(); - + private void executeEffects(WiredStack stack, List effects, WiredContext ctx, long currentTime) { if (effects.isEmpty()) { return; } - - // Selectors already executed before conditions; only run regular effects here - List regulars = new ArrayList<>(); - for (IWiredEffect e : effects) { - if (!e.isSelector()) regulars.add(e); - } // Determine which (regular) effects to execute List toExecute; if (stack.useRandom()) { - // Random mode: pick one random regular effect - if (regulars.isEmpty()) { + WiredExtraRandom randomExtra = getRandomExtra(ctx.room(), stack); + if (effects.isEmpty()) { toExecute = new ArrayList<>(); + } else if (randomExtra != null) { + toExecute = randomExtra.selectWiredEffects(effects); + debug(ctx.room(), "Random mode: selected {} effect(s), skip window {}", toExecute.size(), randomExtra.getSkipExecutions()); } else { - int randomIndex = new Random().nextInt(regulars.size()); - toExecute = Collections.singletonList(regulars.get(randomIndex)); - debug(ctx.room(), "Random mode: selected effect {}/{}", randomIndex + 1, regulars.size()); + int randomIndex = new Random().nextInt(effects.size()); + toExecute = Collections.singletonList(effects.get(randomIndex)); + debug(ctx.room(), "Random mode: selected effect {}/{}", randomIndex + 1, effects.size()); } } else if (stack.useUnseen()) { - // Unseen mode: round-robin among regular effects - if (regulars.isEmpty()) { + // Unseen mode: execute in stable order with memory + if (effects.isEmpty()) { toExecute = new ArrayList<>(); } else { - int index = getNextUnseenIndex(stack, regulars.size()); - toExecute = Collections.singletonList(regulars.get(index)); - debug(ctx.room(), "Unseen mode: selected effect {}/{}", index + 1, regulars.size()); + WiredExtraUnseen unseenExtra = getUnseenExtra(ctx.room(), stack); + + if (unseenExtra != null) { + toExecute = unseenExtra.selectWiredEffects(effects); + + if (!toExecute.isEmpty()) { + int selectedIndex = effects.indexOf(toExecute.get(0)); + debug(ctx.room(), "Unseen mode: selected effect {}/{}", selectedIndex + 1, effects.size()); + } else { + debug(ctx.room(), "Unseen mode: no eligible effect found"); + } + } else { + int index = getNextUnseenIndex(stack, effects.size()); + toExecute = Collections.singletonList(effects.get(index)); + debug(ctx.room(), "Unseen mode fallback: selected effect {}/{}", index + 1, effects.size()); + } } + } else if (stack.executeInOrder()) { + debug(ctx.room(), "Ordered mode: executing effect batches in stack order by delay"); + executeOrderedEffects(effects, ctx, currentTime); + return; } else { - // Normal mode: regular effects in random order - toExecute = new ArrayList<>(regulars); - Collections.shuffle(toExecute); + // Normal mode: preserve the physical stack order. + // This matches the legacy handler behavior and avoids visual/state races + // for combinations such as Move/Rotate + Match To Snapshot in the same stack. + toExecute = new ArrayList<>(effects); } - // Execute selected effects - for (IWiredEffect effect : toExecute) { - // Check if effect requires actor - if (effect.requiresActor() && !ctx.hasActor()) { - continue; - } + WiredMoveCarryHelper.beginMovementCollection(); - // Handle delay - int delay = effect.getDelay(); - if (delay > 0) { - // Schedule delayed execution - scheduleDelayedEffect(effect, ctx, delay, currentTime); - } else { - // Execute immediately - ctx.state().step(); - try { - effect.execute(ctx); - - // Activate box animation after execution - if (effect instanceof InteractionWiredEffect) { - InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; - wiredEffect.setCooldown(currentTime); - wiredEffect.activateBox(ctx.room(), ctx.actor().orElse(null), currentTime); - } - } catch (Exception e) { - LOGGER.warn("Error executing effect: {}", e.getMessage()); + try (WiredInternalVariableSupport.UserMoveBatchScope ignored = WiredInternalVariableSupport.beginUserMoveBatch()) { + // Execute selected effects + for (int effectIndex = 0; effectIndex < toExecute.size(); effectIndex++) { + IWiredEffect effect = toExecute.get(effectIndex); + + // Check if effect requires actor + if (effect.requiresActor() && !ctx.hasActor()) { + continue; } + + // Handle delay + int delay = effect.getDelay(); + if (delay > 0) { + List delayedBatch = new ArrayList<>(); + delayedBatch.add(effect); + + while ((effectIndex + 1) < toExecute.size()) { + IWiredEffect nextEffect = toExecute.get(effectIndex + 1); + + if (nextEffect == null || nextEffect.getDelay() != delay) { + break; + } + + if (nextEffect.requiresActor() && !ctx.hasActor()) { + effectIndex++; + continue; + } + + delayedBatch.add(nextEffect); + effectIndex++; + } + + if (delayedBatch.size() == 1) { + scheduleDelayedEffect(effect, ctx, delay, currentTime); + } else { + scheduleOrderedEffectBatch(delayedBatch, ctx, delay, currentTime); + } + } else { + // Execute immediately + ctx.state().step(); + try { + effect.execute(ctx); + + // Activate box animation after execution + if (effect instanceof InteractionWiredEffect) { + InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; + wiredEffect.setCooldown(currentTime); + wiredEffect.activateBox(ctx.room(), ctx.actor().orElse(null), currentTime); + } + } catch (Exception e) { + LOGGER.warn("Error executing effect: {}", e.getMessage()); + } + } + } + } finally { + ServerMessage movementComposer = WiredMoveCarryHelper.finishMovementCollection(); + if (movementComposer != null) { + ctx.room().sendComposer(movementComposer); } } } @@ -420,9 +1006,11 @@ public final class WiredEngine { /** * Execute selector effects before conditions so ctx.targets() is populated. */ - private void executeSelectors(WiredStack stack, WiredContext ctx, long currentTime) { + private List executeSelectors(WiredStack stack, WiredContext ctx) { List effects = stack.effects(); - if (effects.isEmpty()) return; + if (effects.isEmpty()) return Collections.emptyList(); + + List executedSelectors = new ArrayList<>(); for (IWiredEffect effect : effects) { if (!effect.isSelector()) continue; @@ -433,35 +1021,77 @@ public final class WiredEngine { ctx.state().step(); try { effect.execute(ctx); - if (effect instanceof InteractionWiredEffect) { - InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; - wiredEffect.setCooldown(currentTime); - wiredEffect.activateBox(ctx.room(), ctx.actor().orElse(null), currentTime); + executedSelectors.add((InteractionWiredEffect) effect); } } catch (Exception e) { LOGGER.warn("Error executing selector: {}", e.getMessage()); } } + + return executedSelectors; } - + + private void finalizeSelectors(List executedSelectors, WiredContext ctx, long currentTime) { + if (executedSelectors == null || executedSelectors.isEmpty()) { + return; + } + + Room room = ctx.room(); + RoomUnit actor = ctx.actor().orElse(null); + + for (InteractionWiredEffect wiredEffect : executedSelectors) { + wiredEffect.setCooldown(currentTime); + wiredEffect.activateBox(room, actor, currentTime); + } + } + + private void applySelectionFilterExtras(WiredStack stack, WiredContext ctx, List executedSelectors) { + if (executedSelectors == null || executedSelectors.isEmpty()) { + return; + } + + Room room = ctx.room(); + if (room == null || stack.triggerItem() == null || room.getRoomSpecialTypes() == null) { + return; + } + + WiredSelectionFilterSupport.applySelectorFilters(room, stack.triggerItem(), ctx); + } + /** * Schedule a delayed effect execution. */ - private void scheduleDelayedEffect(IWiredEffect effect, WiredContext ctx, int delay, long currentTime) { + private void scheduleDelayedEffect(IWiredEffect effect, WiredContext ctx, int delay, long triggerTime) { + WiredRoomDiagnostics diagnostics = getDiagnostics(ctx.room().getId()); + String sourceLabel = getMonitorSourceLabel(ctx.triggerItem(), ctx.event()); + int sourceId = getMonitorSourceId(ctx.triggerItem()); + + if (!diagnostics.tryScheduleDelayedEvent( + System.currentTimeMillis(), + sourceLabel, + sourceId, + String.format("Scheduling delayed effect %s with delay %d tick(s)", effect.getClass().getSimpleName(), delay))) { + debug(ctx.room(), "Delayed events cap blocked effect {}", effect.getClass().getSimpleName()); + return; + } + // Delay is in 500ms ticks long delayMs = delay * 500L; + long elapsedSinceTrigger = Math.max(0L, System.currentTimeMillis() - triggerTime); + long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger); Room room = ctx.room(); RoomUnit actor = ctx.actor().orElse(null); - + Emulator.getThreading().run(() -> { if (!room.isLoaded() || room.getHabbos().isEmpty()) { + diagnostics.completeDelayedEvent(); return; } - + try { effect.execute(ctx); - + // Activate box animation after execution if (effect instanceof InteractionWiredEffect) { InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; @@ -470,22 +1100,169 @@ public final class WiredEngine { } } catch (Exception e) { LOGGER.warn("Error executing delayed effect: {}", e.getMessage()); + } finally { + diagnostics.completeDelayedEvent(); } - }, delayMs); + }, remainingDelayMs); + } + + private void executeOrderedEffects(List effects, WiredContext ctx, long currentTime) { + if (effects == null || effects.isEmpty()) { + return; + } + + Map> effectsByDelay = new LinkedHashMap<>(); + + for (IWiredEffect effect : effects) { + if (effect == null) { + continue; + } + + if (effect.requiresActor() && !ctx.hasActor()) { + continue; + } + + effectsByDelay.computeIfAbsent(effect.getDelay(), key -> new ArrayList<>()).add(effect); + } + + for (Map.Entry> entry : effectsByDelay.entrySet()) { + int delay = entry.getKey(); + List batch = entry.getValue(); + + if (batch.isEmpty()) { + continue; + } + + if (delay > 0) { + scheduleOrderedEffectBatch(batch, ctx, delay, currentTime); + } else { + executeOrderedEffectBatch(batch, ctx, currentTime, false); + } + } + } + + /** + * Preview whether a USER_SAYS event should suppress the public room chat output. + * This mirrors trigger and condition eligibility without executing regular effects. + */ + public boolean shouldSuppressUserSaysOutput(WiredEvent event) { + if (event == null || event.getType() != WiredEvent.Type.USER_SAYS) { + return false; + } + + Room room = event.getRoom(); + if (room == null || !room.isLoaded()) { + return false; + } + + List stacks = index.getStacks(room, event.getType()); + if (stacks.isEmpty()) { + return false; + } + + long triggerTime = event.getCreatedAtMs(); + + for (WiredStack stack : stacks) { + if (!(stack.triggerItem() instanceof WiredTriggerHabboSaysKeyword)) { + continue; + } + + WiredTriggerHabboSaysKeyword trigger = (WiredTriggerHabboSaysKeyword) stack.triggerItem(); + if (!trigger.isHideMessage()) { + continue; + } + + try { + if (wouldTriggerStack(stack, event, triggerTime)) { + return true; + } + } catch (WiredLimitException limitEx) { + debug(room, "Suppression preview stopped (limit): {}", limitEx.getMessage()); + } catch (Exception ex) { + LOGGER.warn("Error previewing USER_SAYS suppression in room {}: {}", room.getId(), ex.getMessage()); + } + } + + return false; + } + + private void scheduleOrderedEffectBatch(List batch, WiredContext ctx, int delay, long triggerTime) { + WiredRoomDiagnostics diagnostics = getDiagnostics(ctx.room().getId()); + String sourceLabel = getMonitorSourceLabel(ctx.triggerItem(), ctx.event()); + int sourceId = getMonitorSourceId(ctx.triggerItem()); + + if (!diagnostics.tryScheduleDelayedEvent( + System.currentTimeMillis(), + sourceLabel, + sourceId, + String.format("Scheduling ordered batch with %d effect(s) and delay %d tick(s)", batch.size(), delay))) { + debug(ctx.room(), "Delayed events cap blocked ordered batch with {} effect(s)", batch.size()); + return; + } + + long delayMs = delay * 500L; + long elapsedSinceTrigger = Math.max(0L, System.currentTimeMillis() - triggerTime); + long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger); + Room room = ctx.room(); + + Emulator.getThreading().run(() -> { + if (!room.isLoaded() || room.getHabbos().isEmpty()) { + diagnostics.completeDelayedEvent(); + return; + } + + try { + executeOrderedEffectBatch(batch, ctx, System.currentTimeMillis(), true); + } finally { + diagnostics.completeDelayedEvent(); + } + }, remainingDelayMs); + } + + private void executeOrderedEffectBatch(List batch, WiredContext ctx, long executionTime, boolean useExecutionTimeForCooldown) { + Room room = ctx.room(); + RoomUnit actor = ctx.actor().orElse(null); + + WiredMoveCarryHelper.beginMovementCollection(); + + try (WiredInternalVariableSupport.UserMoveBatchScope ignored = WiredInternalVariableSupport.beginUserMoveBatch()) { + for (IWiredEffect effect : batch) { + try { + if (!useExecutionTimeForCooldown) { + ctx.state().step(); + } + + effect.execute(ctx); + + if (effect instanceof InteractionWiredEffect) { + InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect; + wiredEffect.setCooldown(executionTime); + wiredEffect.activateBox(room, actor, executionTime); + } + } catch (Exception e) { + LOGGER.warn("Error executing ordered effect batch item: {}", e.getMessage()); + } + } + } finally { + ServerMessage movementComposer = WiredMoveCarryHelper.finishMovementCollection(); + if (movementComposer != null) { + room.sendComposer(movementComposer); + } + } } /** * Get the next unseen index for round-robin selection. */ private int getNextUnseenIndex(WiredStack stack, int effectCount) { - String key = stack.triggerItem() != null + String key = stack.triggerItem() != null ? String.valueOf(stack.triggerItem().getId()) : "default"; - + int current = unseenIndices.getOrDefault(key, -1); int next = (current + 1) % effectCount; unseenIndices.put(key, next); - + return next; } @@ -498,7 +1275,7 @@ public final class WiredEngine { // This event is checked for cancellation THashSet legacyEffects = new THashSet<>(); THashSet legacyConditions = new THashSet<>(); - + // Extract effects (all effects should now implement both interfaces) for (IWiredEffect eff : stack.effects()) { if (eff instanceof InteractionWiredEffect) { @@ -510,7 +1287,7 @@ public final class WiredEngine { legacyConditions.add((InteractionWiredCondition) cond); } } - + WiredStackTriggeredEvent triggeredEvent = new WiredStackTriggeredEvent( event.getRoom(), event.getActor().orElse(null), @@ -518,7 +1295,7 @@ public final class WiredEngine { legacyEffects, legacyConditions ); - + return !Emulator.getPluginManager().fireEvent(triggeredEvent).isCancelled(); } return true; @@ -531,7 +1308,7 @@ public final class WiredEngine { if (stack.triggerItem() instanceof InteractionWiredTrigger) { THashSet legacyEffects = new THashSet<>(); THashSet legacyConditions = new THashSet<>(); - + for (IWiredEffect eff : stack.effects()) { if (eff instanceof InteractionWiredEffect) { legacyEffects.add((InteractionWiredEffect) eff); @@ -542,7 +1319,7 @@ public final class WiredEngine { legacyConditions.add((InteractionWiredCondition) cond); } } - + Emulator.getPluginManager().fireEvent(new WiredStackExecutedEvent( event.getRoom(), event.getActor().orElse(null), @@ -557,10 +1334,16 @@ public final class WiredEngine { * Log a debug message if debug mode is enabled. */ private void debug(Room room, String format, Object... args) { - if (WiredManager.isDebugEnabled()) { - String message = String.format(format.replace("{}", "%s"), args); - LOGGER.info("[WiredEngine][Room {}] {}", room.getId(), message); + if (!WiredManager.isDebugEnabled()) { + return; } + + if (!LOGGER.isDebugEnabled()) { + return; + } + + String message = String.format(format.replace("{}", "%s"), args); + LOGGER.debug("[WiredEngine][Room {}] {}", room.getId(), message); } /** @@ -570,10 +1353,10 @@ public final class WiredEngine { if (triggerItem == null || room.getRoomSpecialTypes() == null) { return; } - + THashSet extras = room.getRoomSpecialTypes().getExtras( triggerItem.getX(), triggerItem.getY()); - + if (extras != null) { for (InteractionWiredExtra extra : extras) { extra.activateBox(room, roomUnit, millis); @@ -581,6 +1364,46 @@ public final class WiredEngine { } } + private WiredExtraRandom getRandomExtra(Room room, WiredStack stack) { + InteractionWiredExtra extra = getStackExtra(room, stack, WiredExtraRandom.class); + + return (extra instanceof WiredExtraRandom) ? (WiredExtraRandom) extra : null; + } + + private WiredExtraUnseen getUnseenExtra(Room room, WiredStack stack) { + InteractionWiredExtra extra = getStackExtra(room, stack, WiredExtraUnseen.class); + + return (extra instanceof WiredExtraUnseen) ? (WiredExtraUnseen) extra : null; + } + + private WiredExtraExecutionLimit getExecutionLimitExtra(Room room, WiredStack stack) { + InteractionWiredExtra extra = getStackExtra(room, stack, WiredExtraExecutionLimit.class); + + return (extra instanceof WiredExtraExecutionLimit) ? (WiredExtraExecutionLimit) extra : null; + } + + private InteractionWiredExtra getStackExtra(Room room, WiredStack stack, Class extraClass) { + if (room == null || stack == null || stack.triggerItem() == null || room.getRoomSpecialTypes() == null) { + return null; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras( + stack.triggerItem().getX(), + stack.triggerItem().getY()); + + if (extras == null || extras.isEmpty()) { + return null; + } + + for (InteractionWiredExtra extra : extras) { + if (extraClass.isInstance(extra)) { + return extra; + } + } + + return null; + } + /** * Get the services used by this engine. * @return the wired services @@ -611,7 +1434,7 @@ public final class WiredEngine { public void clearUnseenCache() { unseenIndices.clear(); } - + /** * Clear recursion tracking for a specific room. * Should be called when a room is unloaded. @@ -620,14 +1443,14 @@ public final class WiredEngine { public void clearRoomRecursionDepth(int roomId) { roomRecursionDepth.remove(roomId); } - + /** * Clear all recursion tracking. */ public void clearAllRecursionDepth() { roomRecursionDepth.clear(); } - + /** * Get the current recursion depth for a room (for debugging). * @param roomId the room ID @@ -636,7 +1459,7 @@ public final class WiredEngine { public int getRecursionDepth(int roomId) { return roomRecursionDepth.getOrDefault(roomId, 0); } - + /** * Clear rate limiters for a specific room. * Should be called when a room is unloaded. @@ -646,16 +1469,91 @@ public final class WiredEngine { String prefix = roomId + ":"; eventRateLimiters.keySet().removeIf(key -> key.startsWith(prefix)); } - + + /** + * Clear monitor diagnostics for a specific room. + * @param roomId the room ID + */ + public void clearRoomDiagnostics(int roomId) { + roomDiagnostics.remove(roomId); + } + + /** + * Clear all monitor diagnostics. + */ + public void clearAllDiagnostics() { + roomDiagnostics.clear(); + } + + public void clearRoomDiagnosticsLogs(int roomId) { + WiredRoomDiagnostics diagnostics = roomDiagnostics.get(roomId); + + if (diagnostics != null) { + diagnostics.clearLogs(); + } + } + + /** + * Clear cached source-stack lookups for a specific room. + * @param roomId the room ID + */ + public void clearRoomSourceStackCache(int roomId) { + String prefix = roomId + ":"; + sourceStacksByTriggerKey.keySet().removeIf(key -> key.startsWith(prefix)); + } + + /** + * Clear all cached source-stack lookups. + */ + public void clearAllSourceStackCache() { + sourceStacksByTriggerKey.clear(); + } + + /** + * Clear all execution-related caches for a specific room. + * @param roomId the room ID + */ + public void clearRoomExecutionCaches(int roomId) { + clearRoomRecursionDepth(roomId); + clearRoomRateLimiters(roomId); + clearRoomSourceStackCache(roomId); + } + + /** + * Clear all execution-related caches. + */ + public void clearAllExecutionCaches() { + clearAllRecursionDepth(); + eventRateLimiters.clear(); + clearAllSourceStackCache(); + clearUnseenCache(); + } + /** * Clear room ban for a specific room. - * Should be called when a room is unloaded. * @param roomId the room ID */ public void clearRoomBan(int roomId) { bannedRooms.remove(roomId); } - + + /** + * Get a monitor snapshot for a room. + * @param roomId the room ID + * @return the diagnostics snapshot + */ + public WiredRoomDiagnostics.Snapshot getDiagnosticsSnapshot(int roomId) { + long now = System.currentTimeMillis(); + long killedUntil = bannedRooms.getOrDefault(roomId, 0L); + + return getDiagnostics(roomId).snapshot( + getRecursionDepth(roomId), + MAX_RECURSION_DEPTH, + killedUntil, + now + ); + } + /** * Check if a room is currently banned from wired execution. * @param roomId the room ID @@ -666,25 +1564,29 @@ public final class WiredEngine { if (banExpiry == null) { return false; } - + if (System.currentTimeMillis() >= banExpiry) { - // Ban expired, remove it bannedRooms.remove(roomId); return false; } - + return true; } - + /** - * Ban wired execution in a room for WIRED_BAN_DURATION_MS. - * Sends alerts to all users in the room and a scripter alert to staff. + * Ban wired execution in a room. * @param roomId the room ID - * @param room the room object (for sending alerts) + * @param room the room object */ - private void banRoom(int roomId, Room room) { + private void banRoom(int roomId, Room room, WiredEvent.Type eventType, int eventCount) { long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS; bannedRooms.put(roomId, banExpiry); + getDiagnostics(roomId).recordKilled( + System.currentTimeMillis(), + String.format("Rate limit exceeded for %s with %d event(s) in %dms", eventType.name(), eventCount, RATE_LIMIT_WINDOW_MS), + eventType.name(), + 0 + ); long banMinutes = WIRED_BAN_DURATION_MS / 60000; @@ -710,19 +1612,19 @@ public final class WiredEngine { LOGGER.warn("Wired abuse detected in room {} ({}). Owner: {}. Wired banned for {} minutes.", roomId, room.getName(), room.getOwnerName(), banMinutes); } - + /** * Check if an event should be rate-limited. - * If rate limit exceeded, bans the room and sends alerts. + * Uses a soft limiter only, without banning rooms. * @param roomId the room ID - * @param room the room object (for sending alerts if banned) + * @param room the room object * @param eventType the event type * @return true if the event should be blocked due to rate limiting */ private boolean isRateLimited(int roomId, Room room, WiredEvent.Type eventType) { String key = roomId + ":" + eventType.name(); long now = System.currentTimeMillis(); - + EventRateTracker tracker = eventRateLimiters.compute(key, (k, existing) -> { if (existing == null) { return new EventRateTracker(now); @@ -730,54 +1632,152 @@ public final class WiredEngine { existing.recordEvent(now); return existing; }); - + boolean limited = tracker.isRateLimited(now); if (limited && tracker.shouldBan(now)) { // First time hitting limit in this suppression window - ban the room - banRoom(roomId, room); + banRoom(roomId, room, eventType, tracker.getEventCount()); } return limited; } - + + private WiredRoomDiagnostics getDiagnostics(int roomId) { + return roomDiagnostics.computeIfAbsent(roomId, ignored -> new WiredRoomDiagnostics( + MONITOR_USAGE_WINDOW_MS, + MONITOR_USAGE_LIMIT, + MONITOR_DELAYED_EVENTS_LIMIT, + MONITOR_OVERLOAD_AVERAGE_MS, + MONITOR_OVERLOAD_PEAK_MS, + MONITOR_HEAVY_USAGE_PERCENT, + MONITOR_HEAVY_CONSECUTIVE_WINDOWS, + MONITOR_OVERLOAD_CONSECUTIVE_WINDOWS, + MONITOR_HEAVY_DELAYED_PERCENT, + 200 + )); + } + + private int estimateStackCost(WiredStack stack, int recursionDepth) { + int cost = 1; + + if (stack == null) { + return cost; + } + + cost += Math.max(0, stack.conditions().size()); + + for (IWiredEffect effect : stack.effects()) { + if (effect == null) { + continue; + } + + cost += effect.isSelector() ? 2 : 3; + + if (effect.getDelay() > 0) { + cost += 4; + } + } + + cost += Math.max(0, recursionDepth) * 2; + + return Math.max(1, cost); + } + + private String getMonitorSourceLabel(HabboItem triggerItem, WiredEvent event) { + if (triggerItem != null && triggerItem.getBaseItem() != null && triggerItem.getBaseItem().getInteractionType() != null) { + return triggerItem.getBaseItem().getInteractionType().getName(); + } + + return (event != null && event.getType() != null) ? event.getType().name() : "room"; + } + + private int getMonitorSourceId(HabboItem triggerItem) { + return triggerItem != null ? triggerItem.getId() : 0; + } + + private String buildStackMonitorReason(WiredStack stack, WiredEvent event, int stackCost) { + if (stack == null) { + return String.format("Processing %s with estimated cost %d", event.getType().name(), stackCost); + } + + int selectors = 0; + int delayedEffects = 0; + + for (IWiredEffect effect : stack.effects()) { + if (effect == null) { + continue; + } + + if (effect.isSelector()) { + selectors++; + } + + if (effect.getDelay() > 0) { + delayedEffects++; + } + } + + return String.format( + "Trigger %s with %d condition(s), %d effect(s), %d selector(s), %d delayed effect(s) and estimated cost %d", + event.getType().name(), + stack.conditions().size(), + stack.effects().size(), + selectors, + delayedEffects, + stackCost + ); + } + + private String buildExecutionMonitorReason(WiredStack stack, long elapsedMs) { + if (stack == null) { + return String.format("Execution completed in %dms", elapsedMs); + } + + return String.format( + "Stack with %d condition(s) and %d effect(s) completed in %dms", + stack.conditions().size(), + stack.effects().size(), + elapsedMs + ); + } + /** * Tracks event rate for a specific room + event type combination. */ private static final class EventRateTracker { private long windowStart; private int eventCount; - private boolean banned; - + private boolean warned; + EventRateTracker(long now) { this.windowStart = now; this.eventCount = 1; - this.banned = false; + this.warned = false; } - + synchronized void recordEvent(long now) { - // Reset window if expired if (now - windowStart > RATE_LIMIT_WINDOW_MS) { windowStart = now; eventCount = 1; - // Don't reset banned here - room ban is checked separately + warned = false; } else { eventCount++; } } - + synchronized boolean isRateLimited(long now) { return eventCount > MAX_EVENTS_PER_WINDOW; } - - /** - * Check if this is the first time we've hit the limit (to trigger ban). - * Returns true only once per suppression window. - */ + synchronized boolean shouldBan(long now) { - if (eventCount > MAX_EVENTS_PER_WINDOW && !banned) { - banned = true; + if (eventCount > MAX_EVENTS_PER_WINDOW && !warned) { + warned = true; return true; } return false; } + + synchronized int getEventCount() { + return eventCount; + } } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEvent.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEvent.java index ca3c543b..59ad10fd 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEvent.java @@ -42,6 +42,18 @@ public final class WiredEvent { /** User walks off furniture */ USER_WALKS_OFF(WiredTriggerType.WALKS_OFF_FURNI), + + /** User clicks furniture */ + USER_CLICKS_FURNI(WiredTriggerType.CLICKS_FURNI), + + /** User clicks invisible click tile furniture */ + USER_CLICKS_TILE(WiredTriggerType.CLICKS_TILE), + + /** User clicks another user */ + USER_CLICKS_USER(WiredTriggerType.CLICKS_USER), + + /** User performs an avatar action */ + USER_PERFORMS_ACTION(WiredTriggerType.USER_PERFORMS_ACTION), /** Furniture state is toggled/changed */ FURNI_STATE_CHANGED(WiredTriggerType.STATE_CHANGED), @@ -51,12 +63,24 @@ public final class WiredEvent { /** Timer fires periodically/repeatedly */ TIMER_REPEAT(WiredTriggerType.PERIODICALLY), + + /** Up-counter reaches a configured elapsed time */ + CLOCK_COUNTER_REACHED(WiredTriggerType.CLOCK_COUNTER), + + /** A user, furni or global variable changed */ + VARIABLE_CHANGED(WiredTriggerType.VARIABLE_CHANGED), /** Long timer repeat */ TIMER_REPEAT_LONG(WiredTriggerType.PERIODICALLY_LONG), + + /** Short timer repeat */ + TIMER_REPEAT_SHORT(WiredTriggerType.PERIODICALLY_SHORT), /** User enters the room */ USER_ENTERS_ROOM(WiredTriggerType.ENTER_ROOM), + + /** User leaves the room */ + USER_LEAVES_ROOM(WiredTriggerType.LEAVE_ROOM), /** Game starts */ GAME_STARTS(WiredTriggerType.GAME_STARTS), @@ -129,9 +153,17 @@ public final class WiredEvent { } } + public enum VariableChangeKind { + NONE, + INCREASED, + DECREASED, + UNCHANGED + } + private final Type type; private final Room room; private final RoomUnit actor; // nullable - the user/bot that caused the event + private final RoomUnit originActor; // nullable - original user that started the chain, preserved across signals private final HabboItem sourceItem; // nullable - the furniture involved private final RoomTile tile; // nullable - the tile where event occurred private final String text; // nullable - text for say triggers @@ -141,6 +173,18 @@ public final class WiredEvent { private final boolean triggeredByEffect; // true if triggered by a wired effect (to prevent loops) private final int callStackDepth; // recursion depth for trigger stacks effect private final int signalChannel; // channel for signal routing (0-based) + private final int actionId; // user action id for USER_PERFORMS_ACTION + private final int actionParameter; // sign/dance parameter when relevant + private final int chatType; // RoomChatType metadata for USER_SAYS + private final int chatStyle; // bubble style for USER_SAYS + private final int signalUserCount; // forwarded users in SIGNAL_RECEIVED + private final int signalFurniCount; // forwarded furni in SIGNAL_RECEIVED + private final int variableTargetType; + private final int variableDefinitionItemId; + private final boolean variableCreated; + private final boolean variableDeleted; + private final VariableChangeKind variableChangeKind; + private final WiredContextVariableScope contextVariableScope; private final long createdAtMs; private WiredEvent(Builder builder) { @@ -148,6 +192,7 @@ public final class WiredEvent { this.room = builder.room; this.actor = builder.actor; this.sourceItem = builder.sourceItem; + this.originActor = builder.originActor; this.tile = builder.tile; this.text = builder.text; this.targetUnit = builder.targetUnit; @@ -156,6 +201,18 @@ public final class WiredEvent { this.triggeredByEffect = builder.triggeredByEffect; this.callStackDepth = builder.callStackDepth; this.signalChannel = builder.signalChannel; + this.actionId = builder.actionId; + this.actionParameter = builder.actionParameter; + this.chatType = builder.chatType; + this.chatStyle = builder.chatStyle; + this.signalUserCount = builder.signalUserCount; + this.signalFurniCount = builder.signalFurniCount; + this.variableTargetType = builder.variableTargetType; + this.variableDefinitionItemId = builder.variableDefinitionItemId; + this.variableCreated = builder.variableCreated; + this.variableDeleted = builder.variableDeleted; + this.variableChangeKind = builder.variableChangeKind; + this.contextVariableScope = builder.contextVariableScope; this.createdAtMs = builder.createdAtMs; } @@ -185,6 +242,17 @@ public final class WiredEvent { return Optional.ofNullable(actor); } + /** + * Get the original actor that started the current event chain. + * For signal events this can differ from {@link #getActor()} when the + * signal forwards users one-by-one but still needs to preserve who + * originally triggered the chain. + * @return optional containing the original actor, or empty if unavailable + */ + public Optional getOriginActor() { + return Optional.ofNullable(originActor); + } + /** * Get the source item that was involved in this event. * For example, the furniture that was clicked or stepped on. @@ -258,6 +326,54 @@ public final class WiredEvent { return signalChannel; } + public int getActionId() { + return actionId; + } + + public int getActionParameter() { + return actionParameter; + } + + public int getChatType() { + return chatType; + } + + public int getChatStyle() { + return chatStyle; + } + + public int getSignalUserCount() { + return signalUserCount; + } + + public int getSignalFurniCount() { + return signalFurniCount; + } + + public int getVariableTargetType() { + return variableTargetType; + } + + public int getVariableDefinitionItemId() { + return variableDefinitionItemId; + } + + public boolean isVariableCreated() { + return variableCreated; + } + + public boolean isVariableDeleted() { + return variableDeleted; + } + + public VariableChangeKind getVariableChangeKind() { + return variableChangeKind; + } + + public WiredContextVariableScope getContextVariableScope() { + return contextVariableScope; + } + /** * Get the timestamp when this event was created. * @return milliseconds since epoch @@ -304,6 +420,7 @@ public final class WiredEvent { private final Type type; private final Room room; private RoomUnit actor; + private RoomUnit originActor; private HabboItem sourceItem; private RoomTile tile; private String text; @@ -313,6 +430,18 @@ public final class WiredEvent { private boolean triggeredByEffect; private int callStackDepth; private int signalChannel; + private int actionId; + private int actionParameter = -1; + private int chatType = -1; + private int chatStyle = -1; + private int signalUserCount; + private int signalFurniCount; + private int variableTargetType = -1; + private int variableDefinitionItemId; + private boolean variableCreated; + private boolean variableDeleted; + private VariableChangeKind variableChangeKind = VariableChangeKind.NONE; + private WiredContextVariableScope contextVariableScope; private long createdAtMs = System.currentTimeMillis(); private Builder(Type type, Room room) { @@ -332,6 +461,11 @@ public final class WiredEvent { return this; } + public Builder originActor(RoomUnit originActor) { + this.originActor = originActor; + return this; + } + /** * Set the source item involved in this event. * @param sourceItem the habbo item @@ -417,6 +551,66 @@ public final class WiredEvent { return this; } + public Builder actionId(int actionId) { + this.actionId = actionId; + return this; + } + + public Builder actionParameter(int actionParameter) { + this.actionParameter = actionParameter; + return this; + } + + public Builder chatType(int chatType) { + this.chatType = chatType; + return this; + } + + public Builder chatStyle(int chatStyle) { + this.chatStyle = chatStyle; + return this; + } + + public Builder signalUserCount(int signalUserCount) { + this.signalUserCount = Math.max(0, signalUserCount); + return this; + } + + public Builder signalFurniCount(int signalFurniCount) { + this.signalFurniCount = Math.max(0, signalFurniCount); + return this; + } + + public Builder variableTargetType(int variableTargetType) { + this.variableTargetType = variableTargetType; + return this; + } + + public Builder variableDefinitionItemId(int variableDefinitionItemId) { + this.variableDefinitionItemId = Math.max(0, variableDefinitionItemId); + return this; + } + + public Builder variableCreated(boolean variableCreated) { + this.variableCreated = variableCreated; + return this; + } + + public Builder variableDeleted(boolean variableDeleted) { + this.variableDeleted = variableDeleted; + return this; + } + + public Builder variableChangeKind(VariableChangeKind variableChangeKind) { + this.variableChangeKind = (variableChangeKind != null) ? variableChangeKind : VariableChangeKind.NONE; + return this; + } + + public Builder contextVariableScope(WiredContextVariableScope contextVariableScope) { + this.contextVariableScope = contextVariableScope; + return this; + } + /** * Set a custom creation timestamp. * @param createdAtMs milliseconds since epoch diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredExecutionOrderUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredExecutionOrderUtil.java new file mode 100644 index 00000000..2b2bf606 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredExecutionOrderUtil.java @@ -0,0 +1,34 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.users.HabboItem; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +public final class WiredExecutionOrderUtil { + private static final Comparator WIRED_STACK_ORDER = Comparator + .comparingDouble(HabboItem::getZ) + .thenComparingInt(HabboItem::getId); + + private WiredExecutionOrderUtil() { + } + + public static List sort(Collection items) { + List sorted = new ArrayList<>(); + + if (items == null || items.isEmpty()) { + return sorted; + } + + for (T item : items) { + if (item != null) { + sorted.add(item); + } + } + + sorted.sort(WIRED_STACK_ORDER); + return sorted; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredFreezeUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredFreezeUtil.java new file mode 100644 index 00000000..70b73254 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredFreezeUtil.java @@ -0,0 +1,74 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer; + +public final class WiredFreezeUtil { + private static final String CACHE_ACTIVE = "wired.freeze.active"; + private static final String CACHE_EFFECT_ID = "wired.freeze.effect_id"; + private static final String CACHE_CANCEL_ON_TELEPORT = "wired.freeze.cancel_on_teleport"; + + private WiredFreezeUtil() { + } + + public static boolean isFrozen(RoomUnit roomUnit) { + return roomUnit != null && Boolean.TRUE.equals(roomUnit.getCacheable().get(CACHE_ACTIVE)); + } + + public static void freeze(Room room, RoomUnit roomUnit, int effectId, boolean cancelOnTeleport) { + if (room == null || roomUnit == null || effectId <= 0) { + return; + } + + roomUnit.getCacheable().put(CACHE_ACTIVE, true); + roomUnit.getCacheable().put(CACHE_EFFECT_ID, effectId); + roomUnit.getCacheable().put(CACHE_CANCEL_ON_TELEPORT, cancelOnTeleport); + + roomUnit.stopWalking(); + roomUnit.setCanWalk(false); + roomUnit.statusUpdate(true); + + room.giveEffect(roomUnit, effectId, Integer.MAX_VALUE); + room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); + } + + public static void unfreeze(Room room, RoomUnit roomUnit) { + if (roomUnit == null) { + return; + } + + roomUnit.getCacheable().remove(CACHE_ACTIVE); + roomUnit.getCacheable().remove(CACHE_EFFECT_ID); + roomUnit.getCacheable().remove(CACHE_CANCEL_ON_TELEPORT); + + roomUnit.stopWalking(); + roomUnit.setCanWalk(true); + roomUnit.statusUpdate(true); + + if (room != null) { + room.giveEffect(roomUnit, 0, -1); + room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); + } else { + roomUnit.setEffectId(0, 0); + } + } + + public static void onTeleport(Room room, RoomUnit roomUnit) { + if (!isFrozen(roomUnit)) { + return; + } + + if (Boolean.TRUE.equals(roomUnit.getCacheable().get(CACHE_CANCEL_ON_TELEPORT))) { + unfreeze(room, roomUnit); + } + } + + public static void restoreWalkState(RoomUnit roomUnit) { + if (roomUnit == null) { + return; + } + + roomUnit.setCanWalk(!isFrozen(roomUnit)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredInternalVariableSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredInternalVariableSupport.java new file mode 100644 index 00000000..24f70935 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredInternalVariableSupport.java @@ -0,0 +1,787 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.pets.Pet; +import com.eu.habbo.habbohotel.rooms.FurnitureMovementError; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomRightLevels; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomTileState; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.rooms.RoomUserRotation; +import com.eu.habbo.habbohotel.users.DanceType; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboGender; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.util.HotelDateTimeUtil; + +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.Locale; + +public final class WiredInternalVariableSupport { + private static final ThreadLocal USER_MOVE_INSTANT_OVERRIDE = new ThreadLocal<>(); + private static final ThreadLocal USER_MOVE_BATCH = new ThreadLocal<>(); + private static final ThreadLocal USER_MOVE_BATCH_DEPTH = new ThreadLocal<>(); + + private WiredInternalVariableSupport() { + } + + public static String normalizeKey(String key) { + if (key == null) { + return ""; + } + + String normalized = key.trim(); + + return switch (normalized) { + case "@position.x" -> "@position_x"; + case "@position.y" -> "@position_y"; + case "@effect" -> "@effect_id"; + case "@handitems" -> "@handitem_id"; + case "@is_mute" -> "@is_muted"; + case "@teams.red.score" -> "@team_red_score"; + case "@teams.green.score" -> "@team_green_score"; + case "@teams.blue.score" -> "@team_blue_score"; + case "@teams.yellow.score" -> "@team_yellow_score"; + case "@teams.red.size" -> "@team_red_size"; + case "@teams.green.size" -> "@team_green_size"; + case "@teams.blue.size" -> "@team_blue_size"; + case "@teams.yellow.size" -> "@team_yellow_size"; + default -> normalized; + }; + } + + public static boolean canUseUserDestination(String key) { + String normalized = normalizeKey(key); + return "@position_x".equals(normalized) || "@position_y".equals(normalized) || "@direction".equals(normalized); + } + + public static boolean canUseFurniDestination(String key) { + String normalized = normalizeKey(key); + return "@state".equals(normalized) || "@position_x".equals(normalized) || "@position_y".equals(normalized) + || "@rotation".equals(normalized) || "@altitude".equals(normalized); + } + + public static boolean canUseUserReference(String key) { + String normalized = normalizeKey(key); + + return "@index".equals(normalized) || "@type".equals(normalized) || "@gender".equals(normalized) + || "@level".equals(normalized) || "@achievement_score".equals(normalized) || "@is_hc".equals(normalized) + || "@has_rights".equals(normalized) || "@is_group_admin".equals(normalized) || "@is_owner".equals(normalized) + || "@is_muted".equals(normalized) || "@is_trading".equals(normalized) || "@is_frozen".equals(normalized) + || "@effect_id".equals(normalized) || "@team_score".equals(normalized) || "@team_color".equals(normalized) + || "@team_type".equals(normalized) || "@sign".equals(normalized) || "@dance".equals(normalized) + || "@is_idle".equals(normalized) || "@handitem_id".equals(normalized) || "@position_x".equals(normalized) + || "@position_y".equals(normalized) || "@direction".equals(normalized) || "@altitude".equals(normalized) + || "@favourite_group_id".equals(normalized) || "@room_entry.method".equals(normalized) + || "@room_entry.teleport_id".equals(normalized) || "@user_id".equals(normalized) + || "@bot_id".equals(normalized) || "@pet_id".equals(normalized) || "@pet_owner_id".equals(normalized); + } + + public static boolean canUseFurniReference(String key) { + String normalized = normalizeKey(key); + + return "~teleport.target_id".equals(normalized) || "@id".equals(normalized) || "@class_id".equals(normalized) + || "@height".equals(normalized) || "@state".equals(normalized) || "@position_x".equals(normalized) + || "@position_y".equals(normalized) || "@rotation".equals(normalized) || "@altitude".equals(normalized) + || "@is_invisible".equals(normalized) || "@type".equals(normalized) || "@is_stackable".equals(normalized) + || "@can_stand_on".equals(normalized) || "@can_sit_on".equals(normalized) || "@can_lay_on".equals(normalized) + || "@owner_id".equals(normalized) || "@wallitem_offset".equals(normalized) + || "@dimensions.x".equals(normalized) || "@dimensions.y".equals(normalized); + } + + public static boolean canUseRoomReference(String key) { + String normalized = normalizeKey(key); + + return "@furni_count".equals(normalized) || "@user_count".equals(normalized) || "@wired_timer".equals(normalized) + || "@team_red_score".equals(normalized) || "@team_green_score".equals(normalized) || "@team_blue_score".equals(normalized) + || "@team_yellow_score".equals(normalized) || "@team_red_size".equals(normalized) || "@team_green_size".equals(normalized) + || "@team_blue_size".equals(normalized) || "@team_yellow_size".equals(normalized) || "@room_id".equals(normalized) + || "@group_id".equals(normalized) || "@timezone_server".equals(normalized) || "@timezone_client".equals(normalized) + || "@current_time".equals(normalized) || "@current_time.millisecond_of_second".equals(normalized) + || "@current_time.seconds_of_minute".equals(normalized) || "@current_time.minute_of_hour".equals(normalized) + || "@current_time.hour_of_day".equals(normalized) || "@current_time.day_of_week".equals(normalized) + || "@current_time.day_of_month".equals(normalized) || "@current_time.day_of_year".equals(normalized) + || "@current_time.week_of_year".equals(normalized) || "@current_time.month_of_year".equals(normalized) + || "@current_time.year".equals(normalized); + } + + public static boolean canUseContextReference(String key) { + String normalized = normalizeKey(key); + + return "@selector_furni_count".equals(normalized) || "@selector_user_count".equals(normalized) + || "@signal_furni_count".equals(normalized) || "@signal_user_count".equals(normalized) + || "@antenna_id".equals(normalized) || "@chat_type".equals(normalized) || "@chat_style".equals(normalized); + } + + public static boolean hasUserValue(Room room, RoomUnit roomUnit, String key) { + if (room == null || roomUnit == null) { + return false; + } + + Habbo habbo = room.getHabbo(roomUnit); + Bot bot = room.getBot(roomUnit); + Pet pet = room.getPet(roomUnit); + String normalized = normalizeKey(key); + + return switch (normalized) { + case "@index", "@type", "@level", "@achievement_score", "@position_x", "@position_y", "@direction", "@altitude" -> true; + case "@gender" -> habbo != null || bot != null; + case "@is_hc" -> habbo != null && habbo.getHabboStats().hasActiveClub(); + case "@has_rights" -> habbo != null && (room.hasRights(habbo) || room.getGuildRightLevel(habbo).isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS)); + case "@is_group_admin" -> habbo != null && room.getGuildRightLevel(habbo).isEqualOrGreaterThan(RoomRightLevels.GUILD_ADMIN); + case "@is_owner" -> habbo != null && room.isOwner(habbo); + case "@is_muted" -> (habbo != null && room.isMuted(habbo)) || (pet != null && pet.isMuted()); + case "@is_trading" -> habbo != null && room.getActiveTradeForHabbo(habbo) != null; + case "@is_frozen" -> WiredFreezeUtil.isFrozen(roomUnit); + case "@effect_id" -> roomUnit.getEffectId() > 0; + case "@team_score", "@team_color", "@team_type" -> getTeamEffectData(roomUnit.getEffectId()) != null; + case "@sign" -> roomUnit.hasStatus(RoomUnitStatus.SIGN); + case "@dance" -> roomUnit.getDanceType() != null && roomUnit.getDanceType() != DanceType.NONE; + case "@is_idle" -> roomUnit.isIdle(); + case "@handitem_id" -> roomUnit.getHandItem() > 0; + case "@favourite_group_id" -> habbo != null && habbo.getHabboStats().guild > 0; + case "@room_entry.method" -> habbo != null && hasRoomEntryMethod(habbo); + case "@room_entry.teleport_id" -> habbo != null && habbo.getHabboInfo().getRoomEntryTeleportId() > 0; + case "@user_id" -> habbo != null; + case "@bot_id" -> bot != null; + case "@pet_id" -> pet != null; + case "@pet_owner_id" -> pet != null && pet.getUserId() > 0; + default -> false; + }; + } + + public static boolean hasFurniValue(HabboItem item, String key) { + if (item == null || item.getBaseItem() == null) { + return false; + } + + String normalized = normalizeKey(key); + + return switch (normalized) { + case "@id", "@class_id", "@height", "@state", "@position_x", "@position_y", "@rotation", "@altitude", + "@is_invisible", "@type", "@owner_id", "@dimensions.x", "@dimensions.y" -> true; + case "~teleport.target_id" -> item.getTeleportTargetId() > 0; + case "@wallitem_offset" -> item.getBaseItem().getType() == FurnitureType.WALL; + case "@is_stackable" -> item.getBaseItem().allowStack(); + case "@can_stand_on" -> item.getBaseItem().allowWalk(); + case "@can_sit_on" -> item.getBaseItem().allowSit(); + case "@can_lay_on" -> item.getBaseItem().allowLay(); + default -> false; + }; + } + + public static boolean hasRoomValue(Room room, String key) { + return room != null && canUseRoomReference(key); + } + + public static Integer readUserValue(Room room, RoomUnit roomUnit, String key) { + if (room == null || roomUnit == null) { + return null; + } + + Habbo habbo = room.getHabbo(roomUnit); + Bot bot = room.getBot(roomUnit); + Pet pet = room.getPet(roomUnit); + String normalized = normalizeKey(key); + + return switch (normalized) { + case "@index" -> roomUnit.getId(); + case "@type" -> getUserTypeValue(habbo, bot, pet); + case "@gender" -> getGenderValue(habbo, bot); + case "@level" -> (roomUnit.getRightsLevel() != null) ? roomUnit.getRightsLevel().level : 0; + case "@achievement_score" -> (habbo != null) ? habbo.getHabboStats().getAchievementScore() : null; + case "@is_hc" -> (habbo != null && habbo.getHabboStats().hasActiveClub()) ? 1 : 0; + case "@has_rights" -> (habbo != null && (room.hasRights(habbo) || room.getGuildRightLevel(habbo).isEqualOrGreaterThan(RoomRightLevels.GUILD_RIGHTS))) ? 1 : 0; + case "@is_group_admin" -> (habbo != null && room.getGuildRightLevel(habbo).isEqualOrGreaterThan(RoomRightLevels.GUILD_ADMIN)) ? 1 : 0; + case "@is_owner" -> (habbo != null && room.isOwner(habbo)) ? 1 : 0; + case "@is_muted" -> ((habbo != null && room.isMuted(habbo)) || (pet != null && pet.isMuted())) ? 1 : 0; + case "@is_trading" -> (habbo != null && room.getActiveTradeForHabbo(habbo) != null) ? 1 : 0; + case "@is_frozen" -> WiredFreezeUtil.isFrozen(roomUnit) ? 1 : 0; + case "@effect_id" -> roomUnit.getEffectId(); + case "@team_score" -> getUserTeamScore(room, habbo); + case "@team_color" -> getTeamColorId(roomUnit.getEffectId()); + case "@team_type" -> getTeamTypeId(roomUnit.getEffectId()); + case "@sign" -> parseStatusInteger(roomUnit, RoomUnitStatus.SIGN); + case "@dance" -> (roomUnit.getDanceType() != null) ? roomUnit.getDanceType().getType() : 0; + case "@is_idle" -> roomUnit.isIdle() ? 1 : 0; + case "@handitem_id" -> roomUnit.getHandItem(); + case "@position_x" -> (int) roomUnit.getX(); + case "@position_y" -> (int) roomUnit.getY(); + case "@direction" -> (roomUnit.getBodyRotation() != null) ? (int) roomUnit.getBodyRotation().getValue() : 0; + case "@altitude" -> (int) Math.round(roomUnit.getZ() * 100); + case "@favourite_group_id" -> (habbo != null) ? habbo.getHabboStats().guild : null; + case "@room_entry.method" -> getRoomEntryMethodValue(habbo); + case "@room_entry.teleport_id" -> (habbo != null) ? habbo.getHabboInfo().getRoomEntryTeleportId() : null; + case "@user_id" -> (habbo != null) ? habbo.getHabboInfo().getId() : null; + case "@bot_id" -> (bot != null) ? bot.getId() : null; + case "@pet_id" -> (pet != null) ? pet.getId() : null; + case "@pet_owner_id" -> (pet != null) ? pet.getUserId() : null; + default -> null; + }; + } + + public static boolean writeUserValue(Room room, RoomUnit roomUnit, String key, int value) { + Boolean instantOverride = USER_MOVE_INSTANT_OVERRIDE.get(); + + if (instantOverride != null) { + return writeUserValue(room, roomUnit, key, value, WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION, instantOverride); + } + + return writeUserValue(room, roomUnit, key, value, WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION, false); + } + + public static boolean writeUserValue(Room room, RoomUnit roomUnit, String key, int value, int animationDuration, boolean noAnimation) { + if (room == null || roomUnit == null) { + return false; + } + + String normalized = normalizeKey(key); + + if (stageUserMoveIfPossible(room, roomUnit, normalized, value, animationDuration, noAnimation)) { + return true; + } + + return switch (normalized) { + case "@position_x" -> moveUserTo(room, roomUnit, value, roomUnit.getY(), animationDuration, noAnimation); + case "@position_y" -> moveUserTo(room, roomUnit, roomUnit.getX(), value, animationDuration, noAnimation); + case "@direction" -> { + RoomUserRotation rotation = RoomUserRotation.fromValue(value); + yield WiredUserMovementHelper.updateUserDirection(room, roomUnit, rotation, rotation); + } + default -> false; + }; + } + + public static UserMoveInstantScope beginUserMoveInstantOverride(boolean instant) { + Boolean previousValue = USER_MOVE_INSTANT_OVERRIDE.get(); + USER_MOVE_INSTANT_OVERRIDE.set(instant); + return new UserMoveInstantScope(previousValue); + } + + public static UserMoveBatchScope beginUserMoveBatch() { + Integer previousDepth = USER_MOVE_BATCH_DEPTH.get(); + int nextDepth = (previousDepth == null) ? 1 : (previousDepth + 1); + USER_MOVE_BATCH_DEPTH.set(nextDepth); + + if (nextDepth == 1) { + USER_MOVE_BATCH.set(new UserMoveBatch()); + } + + return new UserMoveBatchScope(previousDepth); + } + + public static Integer readFurniValue(Room room, HabboItem item, String key) { + if (room == null || item == null) { + return null; + } + + String normalized = normalizeKey(key); + + return switch (normalized) { + case "~teleport.target_id" -> item.getTeleportTargetId(); + case "@id" -> item.getId(); + case "@class_id" -> (item.getBaseItem() != null) ? item.getBaseItem().getId() : null; + case "@height" -> (item.getBaseItem() != null) ? (int) Math.round(item.getBaseItem().getHeight() * 100) : null; + case "@state" -> parseInteger(item.getExtradata()); + case "@position_x" -> (int) item.getX(); + case "@position_y" -> (int) item.getY(); + case "@rotation" -> item.getRotation(); + case "@altitude" -> (int) Math.round(item.getZ() * 100); + case "@is_invisible" -> 0; + case "@type" -> 0; + case "@is_stackable" -> (item.getBaseItem() != null && item.getBaseItem().allowStack()) ? 1 : 0; + case "@can_stand_on" -> (item.getBaseItem() != null && item.getBaseItem().allowWalk()) ? 1 : 0; + case "@can_sit_on" -> (item.getBaseItem() != null && item.getBaseItem().allowSit()) ? 1 : 0; + case "@can_lay_on" -> (item.getBaseItem() != null && item.getBaseItem().allowLay()) ? 1 : 0; + case "@wallitem_offset" -> ((item.getBaseItem() != null) && item.getBaseItem().getType() == FurnitureType.WALL && item.getWallPosition() != null && !item.getWallPosition().trim().isEmpty()) ? 1 : 0; + case "@dimensions.x" -> (item.getBaseItem() != null) ? (int) item.getBaseItem().getWidth() : null; + case "@dimensions.y" -> (item.getBaseItem() != null) ? (int) item.getBaseItem().getLength() : null; + case "@owner_id" -> item.getUserId(); + default -> null; + }; + } + + public static boolean writeFurniValue(Room room, HabboItem item, String key, int value) { + if (room == null || item == null) { + return false; + } + + String normalized = normalizeKey(key); + + if ("@state".equals(normalized)) { + item.setExtradata(String.valueOf(normalizeFurniStateValue(item, value))); + room.updateItemState(item); + return true; + } + + if (item.getBaseItem() == null || item.getBaseItem().getType() != FurnitureType.FLOOR) { + return false; + } + + return switch (normalized) { + case "@position_x" -> moveFurniTo(room, item, value, item.getY(), item.getRotation(), item.getZ()); + case "@position_y" -> moveFurniTo(room, item, item.getX(), value, item.getRotation(), item.getZ()); + case "@rotation" -> moveFurniTo(room, item, item.getX(), item.getY(), value, item.getZ()); + case "@altitude" -> moveFurniTo(room, item, item.getX(), item.getY(), item.getRotation(), value / 100.0); + default -> false; + }; + } + + public static Integer readRoomValue(Room room, String key) { + if (room == null) { + return null; + } + + ZonedDateTime now = HotelDateTimeUtil.now(); + String normalized = normalizeKey(key); + + return switch (normalized) { + case "@furni_count" -> room.getFloorItems().size() + room.getWallItems().size(); + case "@user_count" -> room.getUserCount(); + case "@wired_timer" -> (int) (WiredManager.getTickService().getTickCount() / 10L); + case "@team_red_score" -> getTeamMetric(room, GameTeamColors.RED, true); + case "@team_green_score" -> getTeamMetric(room, GameTeamColors.GREEN, true); + case "@team_blue_score" -> getTeamMetric(room, GameTeamColors.BLUE, true); + case "@team_yellow_score" -> getTeamMetric(room, GameTeamColors.YELLOW, true); + case "@team_red_size" -> getTeamMetric(room, GameTeamColors.RED, false); + case "@team_green_size" -> getTeamMetric(room, GameTeamColors.GREEN, false); + case "@team_blue_size" -> getTeamMetric(room, GameTeamColors.BLUE, false); + case "@team_yellow_size" -> getTeamMetric(room, GameTeamColors.YELLOW, false); + case "@room_id" -> room.getId(); + case "@group_id" -> room.getGuildId(); + case "@timezone_server" -> now.getOffset().getTotalSeconds() / 60; + case "@timezone_client" -> 0; + case "@current_time" -> (int) now.toEpochSecond(); + case "@current_time.millisecond_of_second" -> now.getNano() / 1_000_000; + case "@current_time.seconds_of_minute" -> now.getSecond(); + case "@current_time.minute_of_hour" -> now.getMinute(); + case "@current_time.hour_of_day" -> now.getHour(); + case "@current_time.day_of_week" -> now.getDayOfWeek().getValue(); + case "@current_time.day_of_month" -> now.getDayOfMonth(); + case "@current_time.day_of_year" -> now.getDayOfYear(); + case "@current_time.week_of_year" -> now.get(WeekFields.of(Locale.ITALY).weekOfWeekBasedYear()); + case "@current_time.month_of_year" -> now.getMonthValue(); + case "@current_time.year" -> now.getYear(); + default -> null; + }; + } + + public static Integer readContextValue(WiredContext ctx, String key) { + if (ctx == null) { + return null; + } + + String normalized = normalizeKey(key); + + return switch (normalized) { + case "@selector_furni_count" -> countIterable(ctx.targets() != null ? ctx.targets().items() : null); + case "@selector_user_count" -> countIterable(ctx.targets() != null ? ctx.targets().users() : null); + case "@signal_furni_count" -> ctx.event().getSignalFurniCount(); + case "@signal_user_count" -> ctx.event().getSignalUserCount(); + case "@antenna_id" -> ctx.event().getSignalChannel(); + case "@chat_type" -> ctx.event().getChatType(); + case "@chat_style" -> ctx.event().getChatStyle(); + default -> null; + }; + } + + private static Integer getUserTypeValue(Habbo habbo, Bot bot, Pet pet) { + if (habbo != null) return 1; + if (pet != null) return 2; + if (bot != null) return 4; + return null; + } + + private static Integer getGenderValue(Habbo habbo, Bot bot) { + HabboGender gender = null; + + if (habbo != null && habbo.getHabboInfo() != null) { + gender = habbo.getHabboInfo().getGender(); + } else if (bot != null) { + gender = bot.getGender(); + } + + if (gender == null) { + return -1; + } + + return (gender == HabboGender.F) ? 1 : 0; + } + + private static Integer getUserTeamScore(Room room, Habbo habbo) { + if (room == null || habbo == null || habbo.getHabboInfo() == null || habbo.getHabboInfo().getGamePlayer() == null) { + return null; + } + + Game game = resolveTeamGame(room, habbo); + GamePlayer gamePlayer = habbo.getHabboInfo().getGamePlayer(); + + if (game == null || gamePlayer.getTeamColor() == null) { + return gamePlayer.getScore(); + } + + GameTeam team = game.getTeam(gamePlayer.getTeamColor()); + return (team != null) ? team.getTotalScore() : gamePlayer.getScore(); + } + + private static Integer getTeamColorId(int effectId) { + TeamEffectData data = getTeamEffectData(effectId); + return (data != null) ? data.colorId : null; + } + + private static Integer getTeamTypeId(int effectId) { + TeamEffectData data = getTeamEffectData(effectId); + return (data != null) ? data.typeId : null; + } + + private static TeamEffectData getTeamEffectData(int effectId) { + if (effectId <= 0) { + return null; + } + + if (effectId >= 223 && effectId <= 226) return new TeamEffectData(effectId - 222, 0); + if (effectId >= 33 && effectId <= 36) return new TeamEffectData(effectId - 32, 1); + if (effectId >= 40 && effectId <= 43) return new TeamEffectData(effectId - 39, 2); + + return null; + } + + private static int getTeamMetric(Room room, GameTeamColors color, boolean score) { + Game game = resolveTeamGame(room, null); + if (game == null || color == null) { + return 0; + } + + GameTeam team = game.getTeam(color); + if (team == null) { + return 0; + } + + return score ? team.getTotalScore() : team.getMembers().size(); + } + + private static Game resolveTeamGame(Room room, Habbo habbo) { + if (room == null) { + return null; + } + + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getCurrentGame() != null) { + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (game != null) { + return game; + } + } + + Game game = room.getGame(com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame.class); + if (game != null) { + return game; + } + + game = room.getGame(com.eu.habbo.habbohotel.games.freeze.FreezeGame.class); + if (game != null) { + return game; + } + + return room.getGame(com.eu.habbo.habbohotel.games.wired.WiredGame.class); + } + + private static boolean hasRoomEntryMethod(Habbo habbo) { + if (habbo == null || habbo.getHabboInfo() == null) { + return false; + } + + String roomEntryMethod = habbo.getHabboInfo().getRoomEntryMethod(); + return roomEntryMethod != null && !roomEntryMethod.trim().isEmpty() && !"unknown".equalsIgnoreCase(roomEntryMethod); + } + + private static Integer getRoomEntryMethodValue(Habbo habbo) { + if (habbo == null || habbo.getHabboInfo() == null) { + return null; + } + + String roomEntryMethod = habbo.getHabboInfo().getRoomEntryMethod(); + + if (roomEntryMethod == null || roomEntryMethod.trim().isEmpty()) { + return 0; + } + + return switch (roomEntryMethod.trim().toLowerCase(Locale.ROOT)) { + case "door" -> 1; + case "teleport" -> 2; + default -> 3; + }; + } + + private static int parseStatusInteger(RoomUnit roomUnit, RoomUnitStatus status) { + if (roomUnit == null || status == null || !roomUnit.hasStatus(status)) { + return 0; + } + + return parseInteger(roomUnit.getStatus(status)); + } + + private static boolean moveUserTo(Room room, RoomUnit roomUnit, int x, int y, int animationDuration, boolean noAnimation) { + if (room == null || roomUnit == null || room.getLayout() == null) { + return false; + } + + RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); + if (targetTile == null || targetTile.state == RoomTileState.INVALID) { + return false; + } + + double targetZ = WiredUserMovementHelper.resolveUserTargetZ(room, targetTile); + return WiredUserMovementHelper.moveUser( + room, + roomUnit, + targetTile, + targetZ, + roomUnit.getBodyRotation(), + roomUnit.getHeadRotation(), + animationDuration, + noAnimation); + } + + private static boolean stageUserMoveIfPossible(Room room, RoomUnit roomUnit, String normalizedKey, int value, int animationDuration, boolean noAnimation) { + if (room == null || roomUnit == null || normalizedKey == null) { + return false; + } + + if (!"@position_x".equals(normalizedKey) && !"@position_y".equals(normalizedKey)) { + return false; + } + + UserMoveBatch batch = USER_MOVE_BATCH.get(); + + if (batch == null) { + return false; + } + + UserMoveBatchEntry entry = batch.entries.computeIfAbsent(roomUnit.getId(), ignored -> + new UserMoveBatchEntry(room, roomUnit, roomUnit.getX(), roomUnit.getY(), animationDuration, noAnimation)); + + entry.animationDuration = animationDuration; + entry.noAnimation = noAnimation; + + if ("@position_x".equals(normalizedKey)) { + entry.targetX = value; + entry.xDirty = true; + } else { + entry.targetY = value; + entry.yDirty = true; + } + + if (entry.xDirty && entry.yDirty && !entry.noAnimation) { + executeUserMoveBatchEntry(entry); + } + + return true; + } + + private static void flushUserMoveBatch(UserMoveBatch batch) { + if (batch == null || batch.entries.isEmpty()) { + return; + } + + for (UserMoveBatchEntry entry : batch.entries.values()) { + executeUserMoveBatchEntry(entry); + } + } + + private static void executeUserMoveBatchEntry(UserMoveBatchEntry entry) { + if (entry == null || entry.room == null || entry.roomUnit == null || entry.room.getLayout() == null) { + return; + } + + if (!entry.xDirty && !entry.yDirty) { + return; + } + + RoomTile targetTile = entry.room.getLayout().getTile((short) entry.targetX, (short) entry.targetY); + + if (targetTile == null || targetTile.state == RoomTileState.INVALID) { + return; + } + + double targetZ = WiredUserMovementHelper.resolveUserTargetZ(entry.room, targetTile); + + WiredUserMovementHelper.moveUser( + entry.room, + entry.roomUnit, + targetTile, + targetZ, + entry.roomUnit.getBodyRotation(), + entry.roomUnit.getHeadRotation(), + entry.animationDuration, + entry.noAnimation); + + entry.targetX = entry.roomUnit.getX(); + entry.targetY = entry.roomUnit.getY(); + entry.xDirty = false; + entry.yDirty = false; + } + + private static boolean moveFurniTo(Room room, HabboItem item, int x, int y, int rotation, double z) { + if (room == null || item == null || room.getLayout() == null) { + return false; + } + + RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); + if (targetTile == null || targetTile.state == RoomTileState.INVALID) { + return false; + } + + FurnitureMovementError error = room.moveFurniTo(item, targetTile, rotation, z, null, true, true); + return error == FurnitureMovementError.NONE; + } + + private static int normalizeFurniStateValue(HabboItem item, int value) { + if (item == null || item.getBaseItem() == null) { + return value; + } + + int stateCount = item.getBaseItem().getStateCount(); + if (stateCount <= 0) { + return value; + } + + int wrappedValue = value % stateCount; + if (wrappedValue < 0) { + wrappedValue += stateCount; + } + + return wrappedValue; + } + + private static int parseInteger(String value) { + try { + return (value == null || value.trim().isEmpty()) ? 0 : Integer.parseInt(value.trim()); + } catch (NumberFormatException exception) { + return 0; + } + } + + private static int countIterable(Iterable values) { + if (values == null) { + return 0; + } + + int count = 0; + + for (Object ignored : values) { + count++; + } + + return count; + } + + private static class TeamEffectData { + final int colorId; + final int typeId; + + TeamEffectData(int colorId, int typeId) { + this.colorId = colorId; + this.typeId = typeId; + } + } + + public static final class UserMoveInstantScope implements AutoCloseable { + private final Boolean previousValue; + private boolean closed; + + private UserMoveInstantScope(Boolean previousValue) { + this.previousValue = previousValue; + } + + @Override + public void close() { + if (this.closed) { + return; + } + + this.closed = true; + + if (this.previousValue == null) { + USER_MOVE_INSTANT_OVERRIDE.remove(); + return; + } + + USER_MOVE_INSTANT_OVERRIDE.set(this.previousValue); + } + } + + public static final class UserMoveBatchScope implements AutoCloseable { + private final Integer previousDepth; + private boolean closed; + + private UserMoveBatchScope(Integer previousDepth) { + this.previousDepth = previousDepth; + } + + @Override + public void close() { + if (this.closed) { + return; + } + + this.closed = true; + + Integer currentDepth = USER_MOVE_BATCH_DEPTH.get(); + + if (currentDepth == null || currentDepth <= 1) { + UserMoveBatch currentBatch = USER_MOVE_BATCH.get(); + + if (currentBatch != null) { + flushUserMoveBatch(currentBatch); + } + + USER_MOVE_BATCH.remove(); + + if (this.previousDepth == null) { + USER_MOVE_BATCH_DEPTH.remove(); + } else { + USER_MOVE_BATCH_DEPTH.set(this.previousDepth); + } + + return; + } + + USER_MOVE_BATCH_DEPTH.set(currentDepth - 1); + } + } + + private static final class UserMoveBatch { + private final java.util.LinkedHashMap entries = new java.util.LinkedHashMap<>(); + } + + private static final class UserMoveBatchEntry { + private final Room room; + private final RoomUnit roomUnit; + private int targetX; + private int targetY; + private int animationDuration; + private boolean noAnimation; + private boolean xDirty; + private boolean yDirty; + + private UserMoveBatchEntry(Room room, RoomUnit roomUnit, int targetX, int targetY, int animationDuration, boolean noAnimation) { + this.room = room; + this.roomUnit = roomUnit; + this.targetX = targetX; + this.targetY = targetY; + this.animationDuration = animationDuration; + this.noAnimation = noAnimation; + this.xDirty = false; + this.yDirty = false; + } + } + +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java index e31b948c..085170d6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java @@ -4,9 +4,13 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.catalog.CatalogItem; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectGiveReward; import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectTriggerStacks; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraExecutionLimit; +import com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerHabboClicksUser; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomWiredDisableSupport; import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.Habbo; @@ -14,6 +18,7 @@ import com.eu.habbo.habbohotel.users.HabboBadge; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.habbohotel.wired.WiredGiveRewardItem; import com.eu.habbo.habbohotel.wired.WiredTriggerType; +import com.eu.habbo.habbohotel.wired.api.WiredStack; import com.eu.habbo.habbohotel.wired.migrate.WiredEvents; import com.eu.habbo.habbohotel.wired.tick.WiredTickService; import com.eu.habbo.habbohotel.wired.tick.WiredTickable; @@ -35,23 +40,28 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Set; /** - * Manager class for the new wired engine system. + * Manager class for the wired runtime. *

- * This class serves as the integration point between the emulator and the new - * wired engine. It provides static methods for triggering events and manages - * the lifecycle of the engine. + * WiredManager is now the sole runtime entrypoint for wired execution. Legacy + * configuration keys are still read for backwards compatibility with existing + * databases, but they no longer switch execution back to {@code WiredHandler}. *

- * + * *

Configuration Options:

*
    - *
  • {@code wired.engine.enabled} - Enable new engine (parallel mode)
  • - *
  • {@code wired.engine.exclusive} - Disable legacy handler when true
  • + *
  • {@code wired.engine.enabled} - Compatibility flag kept for old configs
  • + *
  • {@code wired.engine.exclusive} - Compatibility flag kept for old configs
  • *
  • {@code wired.engine.maxStepsPerStack} - Loop protection limit
  • *
  • {@code wired.engine.debug} - Verbose logging
  • *
- * + * *

Migration Strategy:

*
    *
  1. Set {@code wired.engine.enabled=true} to run both engines in parallel
  2. @@ -59,11 +69,14 @@ import java.sql.SQLException; *
  3. Set {@code wired.engine.exclusive=true} to disable legacy engine
  4. *
  5. Full migration complete - WiredManager is now the only wired engine
  6. *
- * + * * @see WiredEngine * @see WiredEvents */ public final class WiredManager { + private static final String CACHE_LAST_ACTION_ID = "wired.last_user_action.id"; + private static final String CACHE_LAST_ACTION_PARAMETER = "wired.last_user_action.parameter"; + private static final String CACHE_LAST_ACTION_TIMESTAMP = "wired.last_user_action.timestamp"; private static final Logger LOGGER = LoggerFactory.getLogger(WiredManager.class); @@ -74,19 +87,20 @@ public final class WiredManager { public static final String CONFIG_DEBUG = "wired.engine.debug"; // Defaults - private static final boolean DEFAULT_ENABLED = false; - private static final boolean DEFAULT_EXCLUSIVE = false; + private static final boolean DEFAULT_ENABLED = true; + private static final boolean DEFAULT_EXCLUSIVE = true; private static final int DEFAULT_MAX_STEPS = 100; /** The singleton engine instance */ private static volatile WiredEngine engine; - + /** The stack index */ private static volatile RoomWiredStackIndex stackIndex; - + /** Whether the engine is initialized */ private static volatile boolean initialized = false; - + private static final ThreadLocal EVENT_HANDLING_DEPTH = new ThreadLocal<>(); + private static final ThreadLocal> DEFERRED_EFFECT_EVENTS = new ThreadLocal<>(); private WiredManager() { // Static utility class } @@ -112,9 +126,10 @@ public final class WiredManager { // Load configuration boolean enabled = Emulator.getConfig().getBoolean(CONFIG_ENABLED, DEFAULT_ENABLED); + boolean exclusive = Emulator.getConfig().getBoolean(CONFIG_EXCLUSIVE, DEFAULT_EXCLUSIVE); int maxSteps = Emulator.getConfig().getInt(CONFIG_MAX_STEPS, DEFAULT_MAX_STEPS); boolean debug = Emulator.getConfig().getBoolean(CONFIG_DEBUG, false); - + // Load additional configuration MAXIMUM_FURNI_SELECTION = Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5); TELEPORT_DELAY = Emulator.getConfig().getInt("wired.effect.teleport.delay", 500); @@ -128,13 +143,17 @@ public final class WiredManager { stackIndex = new RoomWiredStackIndex(); WiredServices services = DefaultWiredServices.getInstance(); engine = new WiredEngine(services, stackIndex, maxSteps); - + // Start the centralized tick service (50ms interval) WiredTickService.getInstance().start(); initialized = true; - - LOGGER.info("Wired Manager initialized - enabled: {}, maxSteps: {}, debug: {}", + + if (!enabled || !exclusive) { + LOGGER.warn("wired.engine.enabled / wired.engine.exclusive are now compatibility-only flags. WiredManager runs as the exclusive engine runtime."); + } + + LOGGER.info("Wired Manager initialized - enabled: {}, exclusive runtime active, maxSteps: {}, debug: {}", enabled, maxSteps, debug); } @@ -148,16 +167,18 @@ public final class WiredManager { } LOGGER.info("Shutting down Wired Manager..."); - + // Stop the tick service first WiredTickService.getInstance().stop(); - + if (stackIndex != null) { stackIndex.clearAll(); } - + if (engine != null) { engine.clearUnseenCache(); + engine.clearAllDiagnostics(); + engine.clearAllExecutionCaches(); } initialized = false; @@ -169,7 +190,7 @@ public final class WiredManager { * @return true if enabled */ public static boolean isEnabled() { - return Emulator.getConfig().getBoolean(CONFIG_ENABLED, DEFAULT_ENABLED); + return initialized && engine != null; } /** @@ -177,7 +198,7 @@ public final class WiredManager { * @return true if exclusive mode */ public static boolean isExclusive() { - return Emulator.getConfig().getBoolean(CONFIG_EXCLUSIVE, DEFAULT_EXCLUSIVE); + return true; } /** @@ -196,6 +217,27 @@ public final class WiredManager { return stackIndex; } + /** + * Get the current monitor snapshot for a room. + * @param roomId the room ID + * @return the diagnostics snapshot, or null if the engine is unavailable + */ + public static WiredRoomDiagnostics.Snapshot getDiagnosticsSnapshot(int roomId) { + if (engine == null) { + return null; + } + + return engine.getDiagnosticsSnapshot(roomId); + } + + public static void clearDiagnosticsLogs(int roomId) { + if (engine == null) { + return; + } + + engine.clearRoomDiagnosticsLogs(roomId); + } + // ========== Event Triggering Methods ========== /** @@ -204,11 +246,96 @@ public final class WiredManager { * @return true if any stack was triggered */ public static boolean handleEvent(WiredEvent event) { + return handleEvent(event, false); + } + + public static boolean handleEvent(WiredEvent event, boolean negateConditions) { if (!isEnabled() || engine == null) { return false; } - - return engine.handleEvent(event); + + if (event == null || RoomWiredDisableSupport.isWiredDisabled(event.getRoom())) { + return false; + } + + Integer previousDepth = EVENT_HANDLING_DEPTH.get(); + int nextDepth = (previousDepth == null) ? 1 : (previousDepth + 1); + EVENT_HANDLING_DEPTH.set(nextDepth); + + if (previousDepth == null) { + DEFERRED_EFFECT_EVENTS.set(new ArrayDeque<>()); + } + + boolean handled = false; + + try { + handled = engine.handleEvent(event, negateConditions); + + if (nextDepth == 1) { + ArrayDeque deferredEvents = DEFERRED_EFFECT_EVENTS.get(); + + while (deferredEvents != null && !deferredEvents.isEmpty()) { + DeferredEffectEvent deferredEvent = deferredEvents.pollFirst(); + + if (deferredEvent == null || deferredEvent.event == null || RoomWiredDisableSupport.isWiredDisabled(deferredEvent.event.getRoom())) { + continue; + } + + handled = engine.handleEvent(deferredEvent.event, deferredEvent.negateConditions) || handled; + } + } + + return handled; + } finally { + if (previousDepth == null) { + EVENT_HANDLING_DEPTH.remove(); + DEFERRED_EFFECT_EVENTS.remove(); + } else { + EVENT_HANDLING_DEPTH.set(previousDepth); + } + } + } + + public static boolean dispatchEffectTriggeredEvent(WiredEvent event) { + return dispatchEffectTriggeredEvent(event, false); + } + + public static boolean dispatchNegatedEffectTriggeredEvent(WiredEvent event) { + return dispatchEffectTriggeredEvent(event, true); + } + + private static boolean dispatchEffectTriggeredEvent(WiredEvent event, boolean negateConditions) { + if (!isEnabled() || engine == null || event == null || RoomWiredDisableSupport.isWiredDisabled(event.getRoom())) { + return false; + } + + Integer currentDepth = EVENT_HANDLING_DEPTH.get(); + + if (currentDepth == null || currentDepth <= 0) { + return handleEvent(event, negateConditions); + } + + ArrayDeque deferredEvents = DEFERRED_EFFECT_EVENTS.get(); + + if (deferredEvents == null) { + deferredEvents = new ArrayDeque<>(); + DEFERRED_EFFECT_EVENTS.set(deferredEvents); + } + + deferredEvents.addLast(new DeferredEffectEvent(event, negateConditions)); + return true; + } + + /** + * Handle a wired event using the new engine when the source trigger item is already known. + * Used by timed wired to avoid scanning unrelated stacks. + */ + private static boolean handleEventForSourceItem(WiredEvent event, HabboItem sourceItem) { + if (!isEnabled() || engine == null || event == null || sourceItem == null) { + return false; + } + + return engine.handleEventForSourceItem(event, sourceItem.getId()); } /** @@ -218,7 +345,7 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null || item == null) { return false; } - + WiredEvent event = WiredEvents.userWalksOn(room, user, item); return handleEvent(event); } @@ -230,23 +357,108 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null || item == null) { return false; } - + WiredEvent event = WiredEvents.userWalksOff(room, user, item); return handleEvent(event); } + /** + * Trigger when a user clicks furniture. + */ + public static boolean triggerUserClicksFurni(Room room, RoomUnit user, HabboItem item) { + if (!isEnabled() || room == null || user == null || item == null) { + return false; + } + + WiredEvent event = WiredEvents.userClicksFurni(room, user, item); + return handleEvent(event); + } + + public static void queueUserClicksFurni(Room room, RoomUnit user, HabboItem item) { + if (!isEnabled() || room == null || user == null || item == null) { + return; + } + + triggerUserClicksFurni(room, user, item); + } + + public static void cancelPendingUserClicksFurni(Room room, RoomUnit user, HabboItem item) { + // Click furni triggers are now executed immediately. + } + /** + * Trigger when a user clicks invisible click tile furniture. + */ + public static boolean triggerUserClicksTile(Room room, RoomUnit user, HabboItem item) { + if (!isEnabled() || room == null || user == null || item == null) { + return false; + } + + WiredEvent event = WiredEvents.userClicksTile(room, user, item); + return handleEvent(event); + } + + /** + * Trigger when a user clicks another user. + */ + public static boolean triggerUserClicksUser(Room room, RoomUnit clickingUser, RoomUnit clickedUser) { + if (!isEnabled() || room == null || clickingUser == null || clickedUser == null) { + return false; + } + + WiredTriggerHabboClicksUser.clearRuntimeFlags(clickingUser); + WiredEvent event = WiredEvents.userClicksUser(room, clickingUser, clickedUser); + return handleEvent(event); + } + + /** + * Trigger when a user performs an avatar action. + */ + public static boolean triggerUserPerformsAction(Room room, RoomUnit user, int actionId, int actionParameter) { + if (!isEnabled() || room == null || user == null) { + return false; + } + + user.getCacheable().put(CACHE_LAST_ACTION_ID, actionId); + user.getCacheable().put(CACHE_LAST_ACTION_PARAMETER, actionParameter); + user.getCacheable().put(CACHE_LAST_ACTION_TIMESTAMP, System.currentTimeMillis()); + + WiredEvent event = WiredEvents.userPerformsAction(room, user, actionId, actionParameter); + return handleEvent(event); + } + /** * Trigger when a user says something. */ public static boolean triggerUserSays(Room room, RoomUnit user, String message) { + return triggerUserSays(room, user, message, -1, -1); + } + + public static boolean triggerUserSays(Room room, RoomUnit user, String message, int chatType, int chatStyle) { if (!isEnabled() || room == null || user == null) { return false; } - - WiredEvent event = WiredEvents.userSays(room, user, message); + + WiredEvent event = WiredEvents.userSays(room, user, message, chatType, chatStyle); return handleEvent(event); } + public static boolean shouldSuppressUserSaysOutput(Room room, RoomUnit user, String message) { + return shouldSuppressUserSaysOutput(room, user, message, -1, -1); + } + + public static boolean shouldSuppressUserSaysOutput(Room room, RoomUnit user, String message, int chatType, int chatStyle) { + if (!isEnabled() || engine == null || room == null || user == null) { + return false; + } + + if (RoomWiredDisableSupport.isWiredDisabled(room)) { + return false; + } + + WiredEvent event = WiredEvents.userSays(room, user, message, chatType, chatStyle); + return engine.shouldSuppressUserSaysOutput(event); + } + /** * Trigger when a user enters the room. */ @@ -254,11 +466,23 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null) { return false; } - + WiredEvent event = WiredEvents.userEntersRoom(room, user); return handleEvent(event); } + /** + * Trigger when a user leaves the room. + */ + public static boolean triggerUserLeavesRoom(Room room, RoomUnit user) { + if (!isEnabled() || room == null || user == null) { + return false; + } + + WiredEvent event = WiredEvents.userLeavesRoom(room, user); + return handleEvent(event); + } + /** * Trigger when furniture state changes. */ @@ -266,33 +490,96 @@ public final class WiredManager { if (!isEnabled() || room == null || item == null) { return false; } - + WiredEvent event = WiredEvents.furniStateChanged(room, user, item); return handleEvent(event); } + public static boolean triggerUserVariableChanged(Room room, int userId, int definitionItemId, boolean created, boolean deleted, WiredEvent.VariableChangeKind changeKind) { + if (!isEnabled() || room == null || definitionItemId <= 0) { + return false; + } + + Habbo habbo = room.getHabbo(userId); + RoomUnit roomUnit = (habbo != null) ? habbo.getRoomUnit() : null; + WiredEvent event = WiredEvents.userVariableChanged(room, roomUnit, definitionItemId, created, deleted, changeKind); + return handleEvent(event); + } + + public static boolean triggerFurniVariableChanged(Room room, int furniId, int definitionItemId, boolean created, boolean deleted, WiredEvent.VariableChangeKind changeKind) { + if (!isEnabled() || room == null || furniId <= 0 || definitionItemId <= 0) { + return false; + } + + HabboItem item = room.getHabboItem(furniId); + WiredEvent event = WiredEvents.furniVariableChanged(room, item, definitionItemId, created, deleted, changeKind); + return handleEvent(event); + } + + public static boolean triggerRoomVariableChanged(Room room, int definitionItemId, WiredEvent.VariableChangeKind changeKind) { + if (!isEnabled() || room == null || definitionItemId <= 0) { + return false; + } + + WiredEvent event = WiredEvents.roomVariableChanged(room, definitionItemId, changeKind); + return handleEvent(event); + } + /** * Trigger a timer tick. */ public static boolean triggerTimerTick(Room room, HabboItem timerItem) { - if (!isEnabled() || room == null) { + if (!isEnabled() || room == null || timerItem == null) { return false; } - + WiredEvent event = WiredEvents.timerTick(room, timerItem); - return handleEvent(event); + return handleEventForSourceItem(event, timerItem); } /** * Trigger a periodic timer. */ public static boolean triggerTimerRepeat(Room room, HabboItem timerItem) { - if (!isEnabled() || room == null) { + if (!isEnabled() || room == null || timerItem == null) { return false; } - + WiredEvent event = WiredEvents.timerRepeat(room, timerItem); - return handleEvent(event); + return handleEventForSourceItem(event, timerItem); + } + + public static boolean triggerClockCounter(Room room, HabboItem counterItem) { + if (!isEnabled() || room == null || counterItem == null) { + return false; + } + + WiredEvent event = WiredEvents.clockCounter(room, counterItem); + return handleEventForSourceItem(event, counterItem); + } + + /** + * Trigger a long periodic timer. + */ + public static boolean triggerTimerRepeatLong(Room room, HabboItem timerItem) { + if (!isEnabled() || room == null || timerItem == null) { + return false; + } + + WiredEvent event = WiredEvents.timerRepeatLong(room, timerItem); + return handleEventForSourceItem(event, timerItem); + } + + /** + * Trigger a short periodic timer. + */ + public static boolean triggerTimerRepeatShort(Room room, HabboItem timerItem) { + if (!isEnabled() || room == null || timerItem == null) { + return false; + } + + WiredEvent event = WiredEvents.timerRepeatShort(room, timerItem); + return handleEventForSourceItem(event, timerItem); } /** @@ -302,7 +589,7 @@ public final class WiredManager { if (!isEnabled() || room == null) { return false; } - + WiredEvent event = WiredEvents.gameStarts(room); return handleEvent(event); } @@ -314,7 +601,7 @@ public final class WiredManager { if (!isEnabled() || room == null) { return false; } - + WiredEvent event = WiredEvents.gameEnds(room); return handleEvent(event); } @@ -326,7 +613,7 @@ public final class WiredManager { if (!isEnabled() || room == null || botUnit == null) { return false; } - + WiredEvent event = WiredEvents.botCollision(room, botUnit); return handleEvent(event); } @@ -338,7 +625,7 @@ public final class WiredManager { if (!isEnabled() || room == null || botUnit == null) { return false; } - + WiredEvent event = WiredEvents.botReachedFurni(room, botUnit, item); return handleEvent(event); } @@ -350,7 +637,7 @@ public final class WiredManager { if (!isEnabled() || room == null || botUnit == null) { return false; } - + WiredEvent event = WiredEvents.botReachedHabbo(room, botUnit, targetUser); return handleEvent(event); } @@ -366,7 +653,7 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null) { return false; } - + WiredEvent event = WiredEvents.scoreAchieved(room, user, score, scoreAdded); return handleEvent(event); } @@ -378,7 +665,7 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null) { return false; } - + WiredEvent event = WiredEvents.userIdles(room, user); return handleEvent(event); } @@ -390,7 +677,7 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null) { return false; } - + WiredEvent event = WiredEvents.userUnidles(room, user); return handleEvent(event); } @@ -402,7 +689,7 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null) { return false; } - + WiredEvent event = WiredEvents.userStartsDancing(room, user); return handleEvent(event); } @@ -414,7 +701,7 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null) { return false; } - + WiredEvent event = WiredEvents.userStopsDancing(room, user); return handleEvent(event); } @@ -426,7 +713,7 @@ public final class WiredManager { if (!isEnabled() || room == null) { return false; } - + WiredEvent event = WiredEvents.teamWins(room, user); return handleEvent(event); } @@ -438,20 +725,20 @@ public final class WiredManager { if (!isEnabled() || room == null) { return false; } - + WiredEvent event = WiredEvents.teamLoses(room, user); return handleEvent(event); } /** - * Trigger from legacy system for parallel running. - * This allows the new engine to run alongside the old one during migration. + * Compatibility bridge for code paths that still describe themselves as + * legacy-triggered. Execution still goes through the new engine only. */ public static boolean triggerFromLegacy(WiredTriggerType triggerType, RoomUnit roomUnit, Room room, Object[] stuff) { if (!isEnabled() || room == null) { return false; } - + WiredEvent event = WiredEvents.fromLegacy(triggerType, room, roomUnit, stuff); return handleEvent(event); } @@ -463,11 +750,20 @@ public final class WiredManager { * Call this when wired items are added/removed/moved. */ public static void invalidateRoom(Room room) { - if (stackIndex != null && room != null) { + if (room == null) { + return; + } + + if (stackIndex != null) { stackIndex.invalidateAll(room); - if (debugEnabled) { - LOGGER.info("[Wired] Cache invalidated for room {}", room.getId()); - } + } + + if (engine != null) { + engine.clearRoomExecutionCaches(room.getId()); + } + + if (debugEnabled) { + LOGGER.info("[Wired] Cache invalidated for room {}", room.getId()); } } @@ -478,13 +774,25 @@ public final class WiredManager { if (stackIndex != null && room != null && tile != null) { stackIndex.invalidate(room, tile); } + + if (engine != null && room != null) { + engine.clearRoomSourceStackCache(room.getId()); + } } /** * Rebuild the wired index for a room. */ public static void rebuildRoom(Room room) { - if (stackIndex != null && room != null) { + if (room == null) { + return; + } + + if (engine != null) { + engine.clearRoomExecutionCaches(room.getId()); + } + + if (stackIndex != null) { stackIndex.rebuild(room); } } @@ -493,19 +801,19 @@ public final class WiredManager { /** Maximum number of furniture items that can be selected in a single wired component */ public static int MAXIMUM_FURNI_SELECTION = 5; - + /** Delay in milliseconds between teleport executions */ public static int TELEPORT_DELAY = 500; // ========== Debug Mode ========== - + /** Debug mode - when enabled, logs detailed wired execution flow */ private static boolean debugEnabled = false; /** * Enables or disables wired debug mode. * When enabled, detailed execution logs are written to help troubleshoot wired stacks. - * + * * @param enabled true to enable debug logging, false to disable */ public static void setDebugEnabled(boolean enabled) { @@ -514,19 +822,19 @@ public final class WiredManager { LOGGER.info("Wired debug mode ENABLED"); } } - + /** * Checks if wired debug mode is enabled. - * + * * @return true if debug mode is active */ public static boolean isDebugEnabled() { return debugEnabled; } - + /** * Logs a debug message if debug mode is enabled. - * + * * @param message the message to log * @param args optional format arguments */ @@ -537,7 +845,7 @@ public final class WiredManager { } // ========== JSON Utilities ========== - + private static GsonBuilder gsonBuilder = null; private static Gson cachedGson = null; @@ -547,12 +855,12 @@ public final class WiredManager { } return gsonBuilder; } - + /** * Gets a cached Gson instance. This is more efficient than calling * getGsonBuilder().create() multiple times, as Gson instances are thread-safe * and can be reused. - * + * * @return a cached Gson instance */ public static Gson getGson() { @@ -563,56 +871,92 @@ public final class WiredManager { } // ========== Tick Service Integration ========== - + /** * Registers a tickable wired item with the centralized tick service. *

* Call this when a time-based wired trigger is placed in a room or when * a room is loaded. *

- * + * * @param room the room the item is in * @param tickable the tickable item (e.g., WiredTriggerRepeater) */ public static void registerTickable(Room room, WiredTickable tickable) { WiredTickService.getInstance().register(room, tickable); } - + /** * Unregisters a tickable wired item from the tick service. *

* Call this when a time-based wired trigger is picked up or when * a room is unloaded. *

- * + * * @param room the room the item was in * @param tickable the tickable item */ public static void unregisterTickable(Room room, WiredTickable tickable) { WiredTickService.getInstance().unregister(room, tickable); } - + /** * Unregisters all tickables for a room. *

* Call this when a room is unloaded to clean up all tick registrations. *

- * + * * @param room the room */ public static void unregisterRoomTickables(Room room) { WiredTickService.getInstance().unregisterRoom(room); + if (room != null) { + room.getFurniVariableManager().clearTransientAssignments(); + room.getRoomVariableManager().clearTransientAssignments(); + } + + if (engine != null && room != null) { + engine.clearRoomExecutionCaches(room.getId()); + } } - + /** * Gets the tick service instance. - * + * * @return the WiredTickService */ public static WiredTickService getTickService() { return WiredTickService.getInstance(); } + public static boolean isTriggerExecutionAllowed(Room room, HabboItem triggerItem, long timestamp) { + WiredExtraExecutionLimit executionLimit = getExecutionLimitExtra(room, triggerItem); + + return executionLimit == null || executionLimit.canExecuteAt(timestamp); + } + + public static WiredExtraExecutionLimit getExecutionLimitExtra(Room room, HabboItem triggerItem) { + if (room == null || triggerItem == null || room.getRoomSpecialTypes() == null) { + return null; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras( + triggerItem.getX(), + triggerItem.getY()); + + if (extras == null || extras.isEmpty()) { + return null; + } + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraExecutionLimit) { + return (WiredExtraExecutionLimit) extra; + } + } + + return null; + } + // ========== Timer Management ========== /** @@ -620,7 +964,7 @@ public final class WiredManager { *

* This uses the new tick service for managing timer resets. *

- * + * * @param room the room */ public static void resetTimers(Room room) { @@ -644,6 +988,10 @@ public final class WiredManager { * @return true if any effects were executed */ public static boolean executeEffectsAtTiles(THashSet tiles, final RoomUnit roomUnit, final Room room, final int callStackDepth) { + if (tiles == null || tiles.isEmpty() || room == null || engine == null || stackIndex == null) { + return false; + } + for (RoomTile tile : tiles) { if (room != null) { THashSet items = room.getItemsAt(tile); @@ -653,9 +1001,9 @@ public final class WiredManager { if (item instanceof InteractionWiredEffect && !(item instanceof WiredEffectTriggerStacks)) { InteractionWiredEffect effect = (InteractionWiredEffect) item; WiredEvent event = WiredEvent.builder(WiredEvent.Type.CUSTOM, room) - .actor(roomUnit) - .callStackDepth(callStackDepth) - .build(); + .actor(roomUnit) + .callStackDepth(callStackDepth) + .build(); WiredContext ctx = new WiredContext(event, effect, DefaultWiredServices.getInstance(), new WiredState(100)); effect.execute(ctx); effect.setCooldown(millis); @@ -667,17 +1015,93 @@ public final class WiredManager { return true; } + public static boolean executeNegatedStacksAtTiles(THashSet tiles, final RoomUnit roomUnit, final Room room, final int callStackDepth) { + if (tiles == null || tiles.isEmpty() || room == null || engine == null || stackIndex == null) { + return false; + } + + boolean handled = false; + WiredEvent event = WiredEvent.builder(WiredEvent.Type.CUSTOM, room) + .actor(roomUnit) + .callStackDepth(callStackDepth) + .build(); + + for (RoomTile tile : tiles) { + List stacks = stackIndex.getStacksAtTile(room, tile); + if (stacks.isEmpty()) { + continue; + } + + for (WiredStack stack : stacks) { + handled = engine.executeDirectStack(stack, event, true) || handled; + } + } + + return handled; + } + + public static boolean executeNegatedTargetStacks(Iterable triggerItems, final RoomUnit roomUnit, final Room room, final int callStackDepth) { + if (triggerItems == null || room == null || engine == null || stackIndex == null || room.getLayout() == null) { + return false; + } + + boolean handled = false; + Set seenTriggerIds = new HashSet<>(); + WiredEvent event = WiredEvent.builder(WiredEvent.Type.CUSTOM, room) + .actor(roomUnit) + .callStackDepth(callStackDepth) + .build(); + + for (HabboItem triggerItem : triggerItems) { + if (triggerItem == null || !seenTriggerIds.add(triggerItem.getId())) { + continue; + } + + RoomTile tile = room.getLayout().getTile(triggerItem.getX(), triggerItem.getY()); + if (tile == null) { + continue; + } + + List stacks = stackIndex.getStacksAtTile(room, tile); + if (stacks.isEmpty()) { + continue; + } + + for (WiredStack stack : stacks) { + HabboItem stackTriggerItem = stack.triggerItem(); + if (stackTriggerItem == null || stackTriggerItem.getId() != triggerItem.getId()) { + continue; + } + + handled = engine.executeDirectStack(stack, event, true) || handled; + break; + } + } + + return handled; + } + + private static final class DeferredEffectEvent { + private final WiredEvent event; + private final boolean negateConditions; + + private DeferredEffectEvent(WiredEvent event, boolean negateConditions) { + this.event = event; + this.negateConditions = negateConditions; + } + } + // ========== Reward System ========== /** * Asynchronously drops/deletes all rewards given by a specific wired item. * Used when a wired reward box is picked up or reset. - * + * * @param wiredId The ID of the wired item whose rewards should be deleted */ public static void dropRewards(int wiredId) { Emulator.getThreading().run(() -> { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("DELETE FROM wired_rewards_given WHERE wired_item = ?")) { statement.setInt(1, wiredId); statement.execute(); @@ -687,17 +1111,9 @@ public final class WiredManager { }); } - private static void giveReward(Habbo habbo, WiredEffectGiveReward wiredBox, WiredGiveRewardItem reward) { - if (wiredBox.getLimit() > 0) - wiredBox.incrementGiven(); - - final int wiredId = wiredBox.getId(); - final int habboId = habbo.getHabboInfo().getId(); - final int rewardId = reward.id; - final int timestamp = Emulator.getIntUnixTimestamp(); - + private static void persistReward(int wiredId, int habboId, int rewardId, int timestamp) { Emulator.getThreading().run(() -> { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO wired_rewards_given (wired_item, user_id, reward_id, timestamp) VALUES ( ?, ?, ?, ?)")) { statement.setInt(1, wiredId); statement.setInt(2, habboId); @@ -708,75 +1124,125 @@ public final class WiredManager { LOGGER.error("Caught SQL exception", e); } }); + } + private static void completeReward(Habbo habbo, WiredEffectGiveReward wiredBox, WiredGiveRewardItem reward, int successCode) { + if (wiredBox.getLimit() > 0) { + wiredBox.incrementGiven(); + } + + persistReward(wiredBox.getId(), habbo.getHabboInfo().getId(), reward.id, Emulator.getIntUnixTimestamp()); + habbo.getClient().sendResponse(new WiredRewardAlertComposer(successCode)); + } + + private static boolean giveReward(Habbo habbo, WiredEffectGiveReward wiredBox, WiredGiveRewardItem reward) { if (reward.badge) { UserWiredRewardReceived rewardReceived = new UserWiredRewardReceived(habbo, wiredBox, "badge", reward.data); - if (Emulator.getPluginManager().fireEvent(rewardReceived).isCancelled()) - return; + if (Emulator.getPluginManager().fireEvent(rewardReceived).isCancelled()) { + return false; + } - if (rewardReceived.value.isEmpty()) - return; - - if (habbo.getInventory().getBadgesComponent().hasBadge(rewardReceived.value)) - return; + if (rewardReceived.value.isEmpty()) { + return false; + } + + if (habbo.getInventory().getBadgesComponent().hasBadge(rewardReceived.value)) { + habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALREADY_RECEIVED)); + return false; + } HabboBadge badge = new HabboBadge(0, rewardReceived.value, 0, habbo); Emulator.getThreading().run(badge); habbo.getInventory().getBadgesComponent().addBadge(badge); habbo.getClient().sendResponse(new AddUserBadgeComposer(badge)); - habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_RECEIVED_BADGE)); - } else { - String[] data = reward.data.split("#"); - - if (data.length == 2) { - UserWiredRewardReceived rewardReceived = new UserWiredRewardReceived(habbo, wiredBox, data[0], data[1]); - if (Emulator.getPluginManager().fireEvent(rewardReceived).isCancelled()) - return; - - if (rewardReceived.value.isEmpty()) - return; - - if (rewardReceived.type.equalsIgnoreCase("credits")) { - int credits = Integer.parseInt(rewardReceived.value); - habbo.giveCredits(credits); - } else if (rewardReceived.type.equalsIgnoreCase("pixels")) { - int pixels = Integer.parseInt(rewardReceived.value); - habbo.givePixels(pixels); - } else if (rewardReceived.type.startsWith("points")) { - int points = Integer.parseInt(rewardReceived.value); - int type = 5; - - try { - type = Integer.parseInt(rewardReceived.type.replace("points", "")); - } catch (Exception e) { - } - - habbo.givePoints(type, points); - } else if (rewardReceived.type.equalsIgnoreCase("furni")) { - Item baseItem = Emulator.getGameEnvironment().getItemManager().getItem(Integer.parseInt(rewardReceived.value)); - if (baseItem != null) { - HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(habbo.getHabboInfo().getId(), baseItem, 0, 0, ""); - - if (item != null) { - habbo.getClient().sendResponse(new AddHabboItemComposer(item)); - habbo.getClient().getHabbo().getInventory().getItemsComponent().addItem(item); - habbo.getClient().sendResponse(new PurchaseOKComposer(null)); - habbo.getClient().sendResponse(new InventoryRefreshComposer()); - habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_RECEIVED_ITEM)); - } - } - } else if (rewardReceived.type.equalsIgnoreCase("respect")) { - habbo.getHabboStats().respectPointsReceived += Integer.parseInt(rewardReceived.value); - } else if (rewardReceived.type.equalsIgnoreCase("cata")) { - CatalogItem item = Emulator.getGameEnvironment().getCatalogManager().getCatalogItem(Integer.parseInt(rewardReceived.value)); - - if (item != null) { - Emulator.getGameEnvironment().getCatalogManager().purchaseItem(null, item, habbo, 1, "", true); - } - habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_RECEIVED_ITEM)); - } - } + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_BADGE); + return true; } + + String[] data = reward.data.split("#"); + + if (data.length != 2) { + return false; + } + + UserWiredRewardReceived rewardReceived = new UserWiredRewardReceived(habbo, wiredBox, data[0], data[1]); + if (Emulator.getPluginManager().fireEvent(rewardReceived).isCancelled()) { + return false; + } + + if (rewardReceived.value.isEmpty()) { + return false; + } + + if (rewardReceived.type.equalsIgnoreCase("credits")) { + habbo.giveCredits(Integer.parseInt(rewardReceived.value)); + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_ITEM); + return true; + } + + if (rewardReceived.type.equalsIgnoreCase("diamonds") || rewardReceived.type.equalsIgnoreCase("diamond")) { + habbo.givePoints(5, Integer.parseInt(rewardReceived.value)); + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_ITEM); + return true; + } + + if (rewardReceived.type.equalsIgnoreCase("pixels")) { + habbo.givePixels(Integer.parseInt(rewardReceived.value)); + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_ITEM); + return true; + } + + if (rewardReceived.type.startsWith("points")) { + int points = Integer.parseInt(rewardReceived.value); + int type = 5; + + try { + type = Integer.parseInt(rewardReceived.type.replace("points", "")); + } catch (Exception e) { + } + + habbo.givePoints(type, points); + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_ITEM); + return true; + } + + if (rewardReceived.type.equalsIgnoreCase("furni")) { + Item baseItem = Emulator.getGameEnvironment().getItemManager().getItem(Integer.parseInt(rewardReceived.value)); + if (baseItem == null) { + return false; + } + + HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(habbo.getHabboInfo().getId(), baseItem, 0, 0, ""); + if (item == null) { + return false; + } + + habbo.getClient().sendResponse(new AddHabboItemComposer(item)); + habbo.getClient().getHabbo().getInventory().getItemsComponent().addItem(item); + habbo.getClient().sendResponse(new PurchaseOKComposer(null)); + habbo.getClient().sendResponse(new InventoryRefreshComposer()); + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_ITEM); + return true; + } + + if (rewardReceived.type.equalsIgnoreCase("respect")) { + habbo.getHabboStats().respectPointsReceived += Integer.parseInt(rewardReceived.value); + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_ITEM); + return true; + } + + if (rewardReceived.type.equalsIgnoreCase("cata")) { + CatalogItem item = Emulator.getGameEnvironment().getCatalogManager().getCatalogItem(Integer.parseInt(rewardReceived.value)); + if (item == null) { + return false; + } + + Emulator.getGameEnvironment().getCatalogManager().purchaseItem(null, item, habbo, 1, "", true); + completeReward(habbo, wiredBox, reward, WiredRewardAlertComposer.REWARD_RECEIVED_ITEM); + return true; + } + + return false; } public static boolean getReward(Habbo habbo, WiredEffectGiveReward wiredBox) { @@ -843,22 +1309,26 @@ public final class WiredManager { } if (!found) { - giveReward(habbo, wiredBox, item); - return true; + return giveReward(habbo, wiredBox, item); } } + + habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.REWARD_ALL_COLLECTED)); + return false; } else { int randomNumber = Emulator.getRandom().nextInt(101); int count = 0; for (WiredGiveRewardItem item : wiredBox.getRewardItems()) { if (randomNumber >= count && randomNumber <= (count + item.probability)) { - giveReward(habbo, wiredBox, item); - return true; + return giveReward(habbo, wiredBox, item); } count += item.probability; } + + habbo.getClient().sendResponse(new WiredRewardAlertComposer(WiredRewardAlertComposer.UNLUCKY_NO_REWARD)); + return false; } } } @@ -869,4 +1339,3 @@ public final class WiredManager { return false; } } - diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java new file mode 100644 index 00000000..6b552b65 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java @@ -0,0 +1,1108 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraAnimationTime; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraMoveCarryUsers; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraMovePhysics; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraMoveNoAnimation; +import com.eu.habbo.habbohotel.rooms.FurnitureMovementError; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.rooms.RoomUnitType; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.rooms.WiredMovementsComposer; +import com.eu.habbo.messages.outgoing.rooms.items.FloorItemOnRollerComposer; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer; +import gnu.trove.set.hash.THashSet; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class WiredMoveCarryHelper { + private static final double DIRECT_HEIGHT_TOLERANCE = 0.1D; + private static final int STATUS_SUPPRESSION_GRACE_MS = 250; + private static final long USER_FOLLOWER_TTL_MS = 10000L; + private static final ThreadLocal> SUPPRESSED_STATUS_ROOM_UNIT_IDS = new ThreadLocal<>(); + private static final ThreadLocal> COLLECTED_MOVEMENTS = new ThreadLocal<>(); + private static final ThreadLocal MOVEMENT_COLLECTION_DEPTH = new ThreadLocal<>(); + private static final ConcurrentHashMap SUPPRESSED_STATUS_COMPOSER_UNTIL = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap> ACTIVE_USER_FOLLOWERS = new ConcurrentHashMap<>(); + + private WiredMoveCarryHelper() { + } + + public static FurnitureMovementError getMovementError(Room room, HabboItem stackItem, HabboItem movingItem, RoomTile targetTile, int rotation, WiredContext ctx) { + if (room == null || movingItem == null || targetTile == null) { + return FurnitureMovementError.INVALID_MOVE; + } + + if (!hasMovementBehaviorExtra(room, stackItem)) { + return room.furnitureFitsAt(targetTile, movingItem, rotation, true); + } + + CarryContext carryContext = prepareCarryContext(room, stackItem, movingItem, ctx); + WiredMovementPhysics movementPhysics = getMovementPhysics(room, stackItem, movingItem, ctx); + FurnitureMovementError movementError = room.furnitureFitsAtWithPhysics(targetTile, movingItem, rotation, false, movementPhysics); + + if (movementError != FurnitureMovementError.NONE) { + return movementError; + } + + if (!carryContext.active) { + return room.furnitureFitsAtWithPhysics(targetTile, movingItem, rotation, true, movementPhysics); + } + + return getBlockingUnitError(room, movingItem, targetTile, rotation, carryContext, movementPhysics); + } + + public static FurnitureMovementError moveFurni(Room room, HabboItem stackItem, HabboItem movingItem, RoomTile targetTile, int rotation, Habbo actor, boolean sendUpdates, WiredContext ctx) { + return moveFurni(room, stackItem, movingItem, targetTile, rotation, null, actor, sendUpdates, ctx, null, null, WiredMovementsComposer.FURNI_ANCHOR_NONE, 0); + } + + public static FurnitureMovementError moveFurni(Room room, HabboItem stackItem, HabboItem movingItem, RoomTile targetTile, int rotation, Double z, Habbo actor, boolean sendUpdates, WiredContext ctx) { + return moveFurni(room, stackItem, movingItem, targetTile, rotation, z, actor, sendUpdates, ctx, null, null, WiredMovementsComposer.FURNI_ANCHOR_NONE, 0); + } + + public static FurnitureMovementError moveFurni(Room room, HabboItem stackItem, HabboItem movingItem, RoomTile targetTile, int rotation, Double z, Habbo actor, boolean sendUpdates, WiredContext ctx, Integer animationDurationOverride) { + return moveFurni(room, stackItem, movingItem, targetTile, rotation, z, actor, sendUpdates, ctx, animationDurationOverride, null, WiredMovementsComposer.FURNI_ANCHOR_NONE, 0); + } + + public static FurnitureMovementError moveFurni(Room room, HabboItem stackItem, HabboItem movingItem, RoomTile targetTile, int rotation, Double z, Habbo actor, boolean sendUpdates, WiredContext ctx, Integer animationDurationOverride, Integer animationElapsedOverride) { + return moveFurni(room, stackItem, movingItem, targetTile, rotation, z, actor, sendUpdates, ctx, animationDurationOverride, animationElapsedOverride, WiredMovementsComposer.FURNI_ANCHOR_NONE, 0); + } + + public static FurnitureMovementError moveFurni(Room room, HabboItem stackItem, HabboItem movingItem, RoomTile targetTile, int rotation, Double z, Habbo actor, boolean sendUpdates, WiredContext ctx, Integer animationDurationOverride, Integer animationElapsedOverride, int anchorType, int anchorId) { + if (room == null || movingItem == null || targetTile == null) { + return FurnitureMovementError.INVALID_MOVE; + } + + if (!hasMovementBehaviorExtra(room, stackItem)) { + return moveFurniLegacy(room, movingItem, targetTile, rotation, z, actor, sendUpdates); + } + + RoomTile oldLocation = room.getLayout() == null ? null : room.getLayout().getTile(movingItem.getX(), movingItem.getY()); + double oldZ = movingItem.getZ(); + CarryContext carryContext = prepareCarryContext(room, stackItem, movingItem, ctx); + WiredMovementPhysics movementPhysics = getMovementPhysics(room, stackItem, movingItem, ctx); + FurnitureMovementError movementError = room.furnitureFitsAtWithPhysics(targetTile, movingItem, rotation, false, movementPhysics); + + if (movementError != FurnitureMovementError.NONE) { + return movementError; + } + + if (carryContext.active) { + movementError = getBlockingUnitError(room, movingItem, targetTile, rotation, carryContext, movementPhysics); + + if (movementError != FurnitureMovementError.NONE) { + return movementError; + } + } else { + movementError = room.furnitureFitsAtWithPhysics(targetTile, movingItem, rotation, true, movementPhysics); + + if (movementError != FurnitureMovementError.NONE) { + return movementError; + } + } + + boolean useWiredMovements = !hasNoAnimationExtra(room, stackItem); + int animationDuration = animationDurationOverride != null + ? Math.max(50, animationDurationOverride) + : getAnimationDuration(room, stackItem, WiredMovementsComposer.DEFAULT_DURATION); + Set previousSuppressedRoomUnitIds = SUPPRESSED_STATUS_ROOM_UNIT_IDS.get(); + + if (carryContext.active) { + HashSet suppressedRoomUnitIds = previousSuppressedRoomUnitIds == null + ? new HashSet<>() + : new HashSet<>(previousSuppressedRoomUnitIds); + suppressedRoomUnitIds.addAll(carryContext.carriedUserIds); + SUPPRESSED_STATUS_ROOM_UNIT_IDS.set(suppressedRoomUnitIds); + } + + FurnitureMovementError result; + Double targetZ = z; + + if (targetZ == null && movementPhysics.isKeepAltitude()) { + targetZ = oldZ; + } + + try { + result = (targetZ == null) + ? room.moveFurniToWithPhysics(movingItem, targetTile, rotation, actor, !useWiredMovements, false, movementPhysics) + : room.moveFurniToWithPhysics(movingItem, targetTile, rotation, targetZ, actor, !useWiredMovements, false, movementPhysics); + } finally { + if (carryContext.active) { + if (previousSuppressedRoomUnitIds == null || previousSuppressedRoomUnitIds.isEmpty()) { + SUPPRESSED_STATUS_ROOM_UNIT_IDS.remove(); + } else { + SUPPRESSED_STATUS_ROOM_UNIT_IDS.set(previousSuppressedRoomUnitIds); + } + } + } + + if (result == FurnitureMovementError.NONE) { + if (!useWiredMovements) { + applyInstantCarryState(room, movingItem, targetTile, rotation, carryContext); + } else if (oldLocation != null) { + sendAnimatedMove(room, movingItem, oldLocation, oldZ, targetTile, rotation, carryContext, animationDuration, (animationElapsedOverride != null) ? Math.max(0, animationElapsedOverride) : 0, anchorType, anchorId); + } + } + + return result; + } + + private static FurnitureMovementError moveFurniLegacy(Room room, HabboItem movingItem, RoomTile targetTile, int rotation, Double z, Habbo actor, boolean sendUpdates) { + if (room == null || movingItem == null || targetTile == null) { + return FurnitureMovementError.INVALID_MOVE; + } + + RoomTile oldLocation = room.getLayout() == null ? null : room.getLayout().getTile(movingItem.getX(), movingItem.getY()); + double oldZ = movingItem.getZ(); + + FurnitureMovementError result = (z == null) + ? room.moveFurniTo(movingItem, targetTile, rotation, actor, sendUpdates) + : room.moveFurniTo(movingItem, targetTile, rotation, z, actor, sendUpdates, false); + + if (result == FurnitureMovementError.NONE + && !sendUpdates + && oldLocation != null + && (oldLocation.x != targetTile.x || oldLocation.y != targetTile.y || Double.compare(oldZ, movingItem.getZ()) != 0)) { + List collectedMovements = COLLECTED_MOVEMENTS.get(); + + if (collectedMovements != null) { + collectedMovements.add(WiredMovementsComposer.furniMovement( + movingItem.getId(), + oldLocation.x, + oldLocation.y, + targetTile.x, + targetTile.y, + oldZ, + movingItem.getZ(), + movingItem.getRotation(), + WiredMovementsComposer.DEFAULT_DURATION, + 0, + WiredMovementsComposer.FURNI_ANCHOR_NONE, + 0)); + } else { + room.sendComposer(new FloorItemOnRollerComposer(movingItem, null, oldLocation, oldZ, targetTile, movingItem.getZ(), 0, room).compose()); + } + } + + return result; + } + + public static boolean shouldSuppressStatusUpdate(RoomUnit roomUnit) { + if (roomUnit == null) { + return false; + } + + Set suppressedRoomUnitIds = SUPPRESSED_STATUS_ROOM_UNIT_IDS.get(); + return suppressedRoomUnitIds != null && suppressedRoomUnitIds.contains(roomUnit.getId()); + } + + public static boolean shouldSuppressStatusComposer(RoomUnit roomUnit) { + if (roomUnit == null) { + return false; + } + + Long suppressedUntil = SUPPRESSED_STATUS_COMPOSER_UNTIL.get(roomUnit.getId()); + + if (suppressedUntil == null) { + return false; + } + + if (suppressedUntil <= System.currentTimeMillis()) { + SUPPRESSED_STATUS_COMPOSER_UNTIL.remove(roomUnit.getId(), suppressedUntil); + return false; + } + + return true; + } + + public static void clearStatusComposerSuppression(RoomUnit roomUnit) { + if (roomUnit == null) { + return; + } + + SUPPRESSED_STATUS_COMPOSER_UNTIL.remove(roomUnit.getId()); + } + + public static void beginMovementCollection() { + Integer currentDepth = MOVEMENT_COLLECTION_DEPTH.get(); + + if (currentDepth == null || currentDepth <= 0) { + COLLECTED_MOVEMENTS.set(new ArrayList<>()); + MOVEMENT_COLLECTION_DEPTH.set(1); + return; + } + + MOVEMENT_COLLECTION_DEPTH.set(currentDepth + 1); + } + + public static ServerMessage finishMovementCollection() { + Integer currentDepth = MOVEMENT_COLLECTION_DEPTH.get(); + + if (currentDepth != null && currentDepth > 1) { + MOVEMENT_COLLECTION_DEPTH.set(currentDepth - 1); + return null; + } + + List movements = COLLECTED_MOVEMENTS.get(); + COLLECTED_MOVEMENTS.remove(); + MOVEMENT_COLLECTION_DEPTH.remove(); + + if (movements == null || movements.isEmpty()) { + return null; + } + + return new WiredMovementsComposer(movements).compose(); + } + + public static void registerUserFollower(Room room, HabboItem stackItem, HabboItem movingItem, RoomUnit targetUnit, Double zOverride, WiredContext ctx) { + if (room == null || stackItem == null || movingItem == null || targetUnit == null) { + return; + } + + ACTIVE_USER_FOLLOWERS + .computeIfAbsent(targetUnit.getId(), key -> new ConcurrentHashMap<>()) + .compute(movingItem.getId(), (key, existing) -> { + if (existing != null + && existing.roomId == room.getId() + && existing.stackItemId == stackItem.getId()) { + if (existing.zOverride == null && zOverride != null) { + existing.zOverride = zOverride; + } + existing.ctx = ctx; + existing.touch(); + return existing; + } + + return new UserFollowEntry( + room.getId(), + stackItem.getId(), + movingItem.getId(), + zOverride, + ctx); + }); + } + + public static void markUserFollowerProcessed(RoomUnit targetUnit, HabboItem movingItem, long moveStatusTimestamp) { + if (targetUnit == null || movingItem == null || moveStatusTimestamp <= 0L) { + return; + } + + ConcurrentHashMap followers = ACTIVE_USER_FOLLOWERS.get(targetUnit.getId()); + if (followers == null) { + return; + } + + UserFollowEntry entry = followers.get(movingItem.getId()); + if (entry == null) { + return; + } + + entry.markProcessed(moveStatusTimestamp); + } + + public static boolean isUserFollowerProcessed(RoomUnit targetUnit, HabboItem movingItem, long moveStatusTimestamp) { + if (targetUnit == null || movingItem == null || moveStatusTimestamp <= 0L) { + return false; + } + + ConcurrentHashMap followers = ACTIVE_USER_FOLLOWERS.get(targetUnit.getId()); + if (followers == null) { + return false; + } + + UserFollowEntry entry = followers.get(movingItem.getId()); + if (entry == null) { + return false; + } + + return entry.lastProcessedMoveTimestamp == moveStatusTimestamp; + } + + public static void processUserFollowers(Room room, Collection roomUnits) { + if (room == null || roomUnits == null || roomUnits.isEmpty()) { + return; + } + + for (RoomUnit roomUnit : roomUnits) { + if (roomUnit == null) { + continue; + } + + ConcurrentHashMap followers = ACTIVE_USER_FOLLOWERS.get(roomUnit.getId()); + if (followers == null || followers.isEmpty()) { + continue; + } + + if (!roomUnit.hasStatus(RoomUnitStatus.MOVE) || roomUnit.getCurrentLocation() == null) { + ACTIVE_USER_FOLLOWERS.remove(roomUnit.getId(), followers); + continue; + } + + long moveStatusTimestamp = roomUnit.getMoveStatusTimestamp(); + List toRemove = new ArrayList<>(); + + if (shouldSettleFollowersForNewStep(followers, moveStatusTimestamp)) { + settleUserFollowers(room, followers); + } + + List> orderedFollowers = new ArrayList<>(followers.entrySet()); + orderedFollowers.sort(Comparator + .comparingDouble((Map.Entry followerEntry) -> { + UserFollowEntry entry = followerEntry.getValue(); + return (entry != null && entry.zOverride != null) ? entry.zOverride : Double.MAX_VALUE; + }) + .thenComparingInt(Map.Entry::getKey)); + + for (Map.Entry followerEntry : orderedFollowers) { + UserFollowEntry entry = followerEntry.getValue(); + + if (entry == null || entry.roomId != room.getId() || entry.expiresAt < System.currentTimeMillis()) { + toRemove.add(followerEntry.getKey()); + continue; + } + + HabboItem stackItem = room.getHabboItem(entry.stackItemId); + HabboItem movingItem = room.getHabboItem(entry.movingItemId); + + if (stackItem == null || movingItem == null) { + toRemove.add(followerEntry.getKey()); + continue; + } + + if (moveStatusTimestamp <= 0L || moveStatusTimestamp == entry.lastProcessedMoveTimestamp) { + continue; + } + + int animationElapsed = resolveMoveStepElapsed(roomUnit); + int animationDuration = resolveMoveStepDuration(roomUnit); + Double targetZ = resolveFollowerStackZ(room, movingItem, roomUnit.getCurrentLocation(), movingItem.getRotation()); + FurnitureMovementError error = moveFurni(room, stackItem, movingItem, roomUnit.getCurrentLocation(), movingItem.getRotation(), targetZ, null, false, entry.ctx, animationDuration, animationElapsed, WiredMovementsComposer.FURNI_ANCHOR_USER, roomUnit.getId()); + + if (error != FurnitureMovementError.NONE && entry.zOverride != null) { + error = moveFurni(room, stackItem, movingItem, roomUnit.getCurrentLocation(), movingItem.getRotation(), entry.zOverride, null, false, entry.ctx, animationDuration, animationElapsed, WiredMovementsComposer.FURNI_ANCHOR_USER, roomUnit.getId()); + } + + if (error == FurnitureMovementError.INVALID_MOVE) { + toRemove.add(followerEntry.getKey()); + continue; + } + + entry.markProcessed(moveStatusTimestamp); + } + + for (Integer movingItemId : toRemove) { + followers.remove(movingItemId); + } + + purgeExpiredFollowers(roomUnit.getId(), followers, true); + } + } + + public static boolean hasNoAnimationExtra(Room room, HabboItem stackItem) { + return getNoAnimationExtra(room, stackItem) != null; + } + + public static int getAnimationDuration(Room room, HabboItem stackItem, int fallbackDuration) { + WiredExtraAnimationTime extra = getAnimationTimeExtra(room, stackItem); + return (extra != null) ? extra.getDurationMs() : fallbackDuration; + } + + public static WiredMovementPhysics getUserMovementPhysics(Room room, HabboItem stackItem, WiredContext ctx) { + if (room == null || stackItem == null) { + return WiredMovementPhysics.NONE; + } + + return getMovementPhysics(room, stackItem, null, ctx); + } + + public static int resolveMoveStepElapsed(RoomUnit roomUnit) { + if (roomUnit == null) { + return 0; + } + + long moveStatusTimestamp = roomUnit.getMoveStatusTimestamp(); + if (moveStatusTimestamp <= 0L) { + return 0; + } + + return (int) Math.max(0L, Math.min(WiredMovementsComposer.DEFAULT_DURATION, System.currentTimeMillis() - moveStatusTimestamp)); + } + + public static int resolveMoveStepDuration(RoomUnit roomUnit) { + return WiredMovementsComposer.DEFAULT_DURATION; + } + + public static Double resolveFollowerStackZ(Room room, HabboItem movingItem, RoomTile targetTile, int rotation) { + if (room == null || movingItem == null || targetTile == null || room.getLayout() == null) { + return null; + } + + double targetZ = room.getStackHeight(targetTile.x, targetTile.y, false, movingItem); + THashSet occupiedTiles = room.getLayout().getTilesAt( + targetTile, + movingItem.getBaseItem().getWidth(), + movingItem.getBaseItem().getLength(), + rotation); + + if (occupiedTiles == null || occupiedTiles.isEmpty()) { + return targetZ; + } + + for (RoomTile occupiedTile : occupiedTiles) { + if (occupiedTile == null) { + continue; + } + + targetZ = Math.max(targetZ, room.getStackHeight(occupiedTile.x, occupiedTile.y, false, movingItem)); + } + + return targetZ; + } + + private static Integer resolveRemainingMoveDuration(RoomUnit roomUnit, HabboItem stackItem, Room room) { + if (roomUnit == null || stackItem == null || room == null) { + return null; + } + + long moveStatusTimestamp = roomUnit.getMoveStatusTimestamp(); + if (moveStatusTimestamp <= 0L) { + return null; + } + + int configuredDuration = getAnimationDuration(room, stackItem, WiredMovementsComposer.DEFAULT_DURATION); + int remainingStepDuration = (int) Math.max(50L, WiredMovementsComposer.DEFAULT_DURATION - Math.max(0L, System.currentTimeMillis() - moveStatusTimestamp)); + return Math.min(configuredDuration, remainingStepDuration); + } + + private static boolean shouldSettleFollowersForNewStep(ConcurrentHashMap followers, long moveStatusTimestamp) { + if (followers == null || followers.isEmpty() || moveStatusTimestamp <= 0L) { + return false; + } + + for (UserFollowEntry entry : followers.values()) { + if (entry != null && entry.lastProcessedMoveTimestamp > 0L && entry.lastProcessedMoveTimestamp != moveStatusTimestamp) { + return true; + } + } + + return false; + } + + private static void settleUserFollowers(Room room, ConcurrentHashMap followers) { + if (room == null || followers == null || followers.isEmpty()) { + return; + } + + List> entriesToSettle = new ArrayList<>(followers.entrySet()); + entriesToSettle.sort(Comparator + .comparingDouble((Map.Entry followerEntry) -> { + UserFollowEntry entry = followerEntry.getValue(); + return (entry != null && entry.zOverride != null) ? -entry.zOverride : Double.POSITIVE_INFINITY; + }) + .thenComparingInt(Map.Entry::getKey)); + + for (Map.Entry followerEntry : entriesToSettle) { + UserFollowEntry entry = followerEntry.getValue(); + + if (entry == null || entry.roomId != room.getId()) { + continue; + } + + HabboItem movingItem = room.getHabboItem(entry.movingItemId); + HabboItem stackItem = room.getHabboItem(entry.stackItemId); + if (movingItem == null || room.getLayout() == null) { + continue; + } + + RoomTile currentTile = room.getLayout().getTile(movingItem.getX(), movingItem.getY()); + if (currentTile == null) { + continue; + } + + Double targetZ = (double) room.getLayout().getHeightAtSquare(currentTile.x, currentTile.y); + + if (stackItem != null) { + FurnitureMovementError error = moveFurni(room, stackItem, movingItem, currentTile, movingItem.getRotation(), targetZ, null, false, entry.ctx, WiredMovementsComposer.DEFAULT_DURATION, 0, WiredMovementsComposer.FURNI_ANCHOR_NONE, 0); + + if (error == FurnitureMovementError.NONE) { + continue; + } + } + + FurnitureMovementError error = room.moveFurniTo(movingItem, currentTile, movingItem.getRotation(), targetZ, null, true, false); + + if (error != FurnitureMovementError.NONE) { + room.moveFurniTo(movingItem, currentTile, movingItem.getRotation(), null, true, false); + } + } + } + + private static void purgeExpiredFollowers(int roomUnitId, ConcurrentHashMap followers, boolean removeEmpty) { + if (followers == null) { + return; + } + + long now = System.currentTimeMillis(); + followers.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue().expiresAt < now); + + if (removeEmpty && followers.isEmpty()) { + ACTIVE_USER_FOLLOWERS.remove(roomUnitId, followers); + } + } + + private static boolean hasMovementBehaviorExtra(Room room, HabboItem stackItem) { + THashSet extras = getMovementExtras(room, stackItem); + if (extras == null || extras.isEmpty()) { + return false; + } + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraMoveCarryUsers + || extra instanceof WiredExtraMoveNoAnimation + || extra instanceof WiredExtraAnimationTime + || extra instanceof WiredExtraMovePhysics) { + return true; + } + } + + return false; + } + + private static CarryContext prepareCarryContext(Room room, HabboItem stackItem, HabboItem movingItem, WiredContext ctx) { + WiredExtraMoveCarryUsers extra = getActiveExtra(room, stackItem); + + if (extra == null || ctx == null || room.getLayout() == null) { + return CarryContext.disabled(); + } + + RoomTile anchorTile = room.getLayout().getTile(movingItem.getX(), movingItem.getY()); + if (anchorTile == null) { + return CarryContext.disabled(); + } + + THashSet occupiedTiles = room.getLayout().getTilesAt( + anchorTile, + movingItem.getBaseItem().getWidth(), + movingItem.getBaseItem().getLength(), + movingItem.getRotation()); + + if (occupiedTiles == null || occupiedTiles.isEmpty()) { + return CarryContext.disabled(); + } + + Collection targetUsers = resolveUsers(room, ctx, extra.getUserSource()); + if (targetUsers == null || targetUsers.isEmpty()) { + return CarryContext.disabled(); + } + + List carriedUnits = new ArrayList<>(); + HashSet carriedIds = new HashSet<>(); + + for (RoomUnit roomUnit : targetUsers) { + if (!isEligibleUser(room, movingItem, roomUnit, occupiedTiles, extra.getCarryMode())) { + continue; + } + + CarriedRoomUnit carriedRoomUnit = new CarriedRoomUnit( + roomUnit, + roomUnit.getCurrentLocation(), + roomUnit.getZ(), + roomUnit.getZ() - getCarrySurfaceZ(movingItem, roomUnit, roomUnit.getZ()), + roomUnit.getX() - anchorTile.x, + roomUnit.getY() - anchorTile.y); + + carriedUnits.add(carriedRoomUnit); + carriedIds.add(roomUnit.getId()); + } + + if (carriedUnits.isEmpty()) { + return CarryContext.disabled(); + } + + return new CarryContext(true, carriedUnits, carriedIds); + } + + private static Collection resolveUsers(Room room, WiredContext ctx, int userSource) { + if (userSource == WiredExtraMoveCarryUsers.SOURCE_ALL_ROOM_USERS) { + return new ArrayList<>(room.getRoomUnits()); + } + + return WiredSourceUtil.resolveUsers(ctx, userSource); + } + + private static boolean isEligibleUser(Room room, HabboItem movingItem, RoomUnit roomUnit, THashSet occupiedTiles, int carryMode) { + if (roomUnit == null + || roomUnit.getRoomUnitType() != RoomUnitType.USER + || roomUnit.getCurrentLocation() == null + || !roomUnit.isInRoom() + || roomUnit.isWalking()) { + return false; + } + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getRiding() != null) { + return false; + } + + if (!occupiesMovingFurni(occupiedTiles, roomUnit)) { + return false; + } + + if (carryMode == WiredExtraMoveCarryUsers.MODE_DIRECTLY_ON_FURNI) { + return isDirectlyOnMovingFurni(room, movingItem, roomUnit); + } + + return true; + } + + private static boolean isDirectlyOnMovingFurni(Room room, HabboItem movingItem, RoomUnit roomUnit) { + HabboItem topItem = room.getTopItemAt(roomUnit.getX(), roomUnit.getY()); + if (topItem == movingItem) { + return true; + } + + double carrySurfaceZ = getCarrySurfaceZ(movingItem, roomUnit, roomUnit.getZ()); + return Math.abs(roomUnit.getZ() - carrySurfaceZ) <= DIRECT_HEIGHT_TOLERANCE; + } + + private static boolean occupiesMovingFurni(THashSet occupiedTiles, RoomUnit roomUnit) { + for (RoomTile occupiedTile : occupiedTiles) { + if (occupiedTile != null + && occupiedTile.x == roomUnit.getX() + && occupiedTile.y == roomUnit.getY()) { + return true; + } + } + + return false; + } + + private static FurnitureMovementError getBlockingUnitError(Room room, HabboItem movingItem, RoomTile targetTile, int rotation, CarryContext carryContext, WiredMovementPhysics movementPhysics) { + THashSet occupiedTiles = room.getLayout().getTilesAt( + targetTile, + movingItem.getBaseItem().getWidth(), + movingItem.getBaseItem().getLength(), + rotation); + + if (occupiedTiles == null || occupiedTiles.isEmpty()) { + return FurnitureMovementError.NONE; + } + + for (RoomTile tile : occupiedTiles) { + for (RoomUnit roomUnit : room.getRoomUnits(tile)) { + if (roomUnit == null || carryContext.carriedUserIds.contains(roomUnit.getId())) { + continue; + } + + if (movementPhysics.shouldIgnoreUser(roomUnit)) { + continue; + } + + switch (roomUnit.getRoomUnitType()) { + case BOT: + return FurnitureMovementError.TILE_HAS_BOTS; + case PET: + return FurnitureMovementError.TILE_HAS_PETS; + case USER: + default: + return FurnitureMovementError.TILE_HAS_HABBOS; + } + } + } + + return FurnitureMovementError.NONE; + } + + private static void sendAnimatedMove(Room room, HabboItem movingItem, RoomTile oldLocation, double oldZ, RoomTile targetTile, int rotation, CarryContext carryContext, int animationDuration, int animationElapsed, int anchorType, int anchorId) { + List carriedMoves = getCarriedUnitMoves(room, movingItem, targetTile, rotation, carryContext); + List movements = new ArrayList<>(); + movements.add(WiredMovementsComposer.furniMovement( + movingItem.getId(), + oldLocation.x, + oldLocation.y, + targetTile.x, + targetTile.y, + oldZ, + movingItem.getZ(), + movingItem.getRotation(), + animationDuration, + animationElapsed, + anchorType, + anchorId)); + + for (CarriedUnitMove carriedMove : carriedMoves) { + suppressStatusComposer(carriedMove.roomUnit, animationDuration); + movements.add(WiredMovementsComposer.userSlideMovement( + carriedMove.roomUnit.getId(), + carriedMove.oldLocation.x, + carriedMove.oldLocation.y, + carriedMove.destinationTile.x, + carriedMove.destinationTile.y, + carriedMove.oldZ, + carriedMove.newZ, + carriedMove.roomUnit.getBodyRotation().getValue(), + carriedMove.roomUnit.getHeadRotation().getValue(), + animationDuration)); + } + + List collectedMovements = COLLECTED_MOVEMENTS.get(); + + if (collectedMovements != null) { + collectedMovements.addAll(movements); + } else { + room.sendComposer(new WiredMovementsComposer(movements).compose()); + } + + for (CarriedUnitMove carriedMove : carriedMoves) { + updateCarriedUnitState(carriedMove); + } + } + + private static void applyInstantCarryState(Room room, HabboItem movingItem, RoomTile targetTile, int rotation, CarryContext carryContext) { + if (!carryContext.active || room == null || movingItem == null || targetTile == null) { + return; + } + + List carriedMoves = getCarriedUnitMoves(room, movingItem, targetTile, rotation, carryContext); + + for (CarriedUnitMove carriedMove : carriedMoves) { + updateCarriedUnitStateInstant(carriedMove); + + Habbo habbo = room.getHabbo(carriedMove.roomUnit); + if (habbo != null && shouldRefreshPostureWithTileUpdate(carriedMove.roomUnit)) { + THashSet movedHabbos = new THashSet<>(); + movedHabbos.add(habbo); + room.updateHabbosAt(carriedMove.destinationTile.x, carriedMove.destinationTile.y, movedHabbos); + } + + room.sendComposer(new RoomUserStatusComposer(carriedMove.roomUnit).compose()); + } + } + + private static List getCarriedUnitMoves(Room room, HabboItem movingItem, RoomTile targetTile, int rotation, CarryContext carryContext) { + List carriedMoves = new ArrayList<>(); + + if (!carryContext.active) { + return carriedMoves; + } + + THashSet occupiedTiles = room.getLayout().getTilesAt( + targetTile, + movingItem.getBaseItem().getWidth(), + movingItem.getBaseItem().getLength(), + rotation); + + for (CarriedRoomUnit carriedRoomUnit : carryContext.carriedUsers) { + RoomUnit roomUnit = carriedRoomUnit.roomUnit; + + if (roomUnit == null || roomUnit.getCurrentLocation() == null || roomUnit.isWalking()) { + continue; + } + + RoomTile destinationTile = room.getLayout().getTile( + (short) (targetTile.x + carriedRoomUnit.relativeX), + (short) (targetTile.y + carriedRoomUnit.relativeY)); + + if (destinationTile == null || destinationTile.state == null || !occupiedTiles.contains(destinationTile)) { + destinationTile = targetTile; + } + + double carrySurfaceZ = getCarrySurfaceZ(movingItem, roomUnit, carriedRoomUnit.oldZ); + double newZ = carrySurfaceZ + carriedRoomUnit.heightOffset; + carriedMoves.add(new CarriedUnitMove(roomUnit, carriedRoomUnit.oldLocation, carriedRoomUnit.oldZ, destinationTile, newZ)); + } + + return carriedMoves; + } + + private static boolean shouldRefreshPostureWithTileUpdate(RoomUnit roomUnit) { + return roomUnit != null + && (roomUnit.hasStatus(RoomUnitStatus.SIT) || roomUnit.hasStatus(RoomUnitStatus.LAY)); + } + + private static double getCarrySurfaceZ(HabboItem movingItem, RoomUnit roomUnit, double referenceZ) { + if (movingItem == null) { + return referenceZ; + } + + double baseZ = movingItem.getZ(); + double topZ = baseZ + Item.getCurrentHeight(movingItem); + + if (roomUnit != null && (roomUnit.hasStatus(RoomUnitStatus.SIT) || roomUnit.hasStatus(RoomUnitStatus.LAY))) { + return baseZ; + } + + if (movingItem.getBaseItem().allowSit() || movingItem.getBaseItem().allowLay()) { + return (Math.abs(referenceZ - baseZ) <= Math.abs(referenceZ - topZ)) ? baseZ : topZ; + } + + return topZ; + } + + private static void updateCarriedUnitState(CarriedUnitMove carriedMove) { + carriedMove.roomUnit.setLocation(carriedMove.destinationTile); + carriedMove.roomUnit.setZ(carriedMove.newZ); + carriedMove.roomUnit.setLastRollerTime(System.currentTimeMillis()); + carriedMove.roomUnit.setPreviousLocation(carriedMove.destinationTile); + carriedMove.roomUnit.setPreviousLocationZ(carriedMove.newZ); + + if (carriedMove.roomUnit.hasStatus(RoomUnitStatus.SIT)) { + carriedMove.roomUnit.sitUpdate = true; + } + } + + private static void updateCarriedUnitStateInstant(CarriedUnitMove carriedMove) { + carriedMove.roomUnit.setLocation(carriedMove.destinationTile); + carriedMove.roomUnit.setZ(carriedMove.newZ); + carriedMove.roomUnit.setPreviousLocation(carriedMove.destinationTile); + carriedMove.roomUnit.setPreviousLocationZ(carriedMove.newZ); + carriedMove.roomUnit.statusUpdate(false); + + if (carriedMove.roomUnit.hasStatus(RoomUnitStatus.SIT)) { + carriedMove.roomUnit.sitUpdate = true; + } + } + + private static void suppressStatusComposer(RoomUnit roomUnit, int duration) { + if (roomUnit == null) { + return; + } + + long suppressedUntil = System.currentTimeMillis() + Math.max(duration, WiredMovementsComposer.DEFAULT_DURATION) + STATUS_SUPPRESSION_GRACE_MS; + SUPPRESSED_STATUS_COMPOSER_UNTIL.put(roomUnit.getId(), suppressedUntil); + } + + private static WiredExtraMoveCarryUsers getActiveExtra(Room room, HabboItem stackItem) { + THashSet extras = getMovementExtras(room, stackItem); + if (extras == null || extras.isEmpty()) { + return null; + } + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraMoveCarryUsers) { + return (WiredExtraMoveCarryUsers) extra; + } + } + + return null; + } + + private static WiredExtraMoveNoAnimation getNoAnimationExtra(Room room, HabboItem stackItem) { + THashSet extras = getMovementExtras(room, stackItem); + if (extras == null || extras.isEmpty()) { + return null; + } + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraMoveNoAnimation) { + return (WiredExtraMoveNoAnimation) extra; + } + } + + return null; + } + + private static WiredExtraAnimationTime getAnimationTimeExtra(Room room, HabboItem stackItem) { + THashSet extras = getMovementExtras(room, stackItem); + if (extras == null || extras.isEmpty()) { + return null; + } + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraAnimationTime) { + return (WiredExtraAnimationTime) extra; + } + } + + return null; + } + + private static WiredMovementPhysics getMovementPhysics(Room room, HabboItem stackItem, HabboItem movingItem, WiredContext ctx) { + WiredExtraMovePhysics extra = getMovementPhysicsExtra(room, stackItem); + if (extra == null) { + return WiredMovementPhysics.NONE; + } + + HashSet passThroughFurniIds = new HashSet<>(); + HashSet passThroughUserIds = new HashSet<>(); + HashSet blockingFurniIds = new HashSet<>(); + + if (extra.isMoveThroughFurni()) { + for (HabboItem item : resolveFurniSources(room, ctx, extra.getMoveThroughFurniSource())) { + if (item != null && item != movingItem) { + passThroughFurniIds.add(item.getId()); + } + } + } + + if (extra.isMoveThroughUsers()) { + for (RoomUnit roomUnit : resolvePhysicsUsers(room, ctx, extra.getMoveThroughUsersSource())) { + if (roomUnit != null && roomUnit.getRoomUnitType() == RoomUnitType.USER) { + passThroughUserIds.add(roomUnit.getId()); + } + } + } + + if (extra.isBlockByFurni()) { + for (HabboItem item : resolveFurniSources(room, ctx, extra.getBlockByFurniSource())) { + if (item != null && item != movingItem) { + blockingFurniIds.add(item.getId()); + } + } + } + + return new WiredMovementPhysics(extra.isKeepAltitude(), passThroughFurniIds, passThroughUserIds, blockingFurniIds); + } + + private static Collection resolveFurniSources(Room room, WiredContext ctx, int sourceType) { + if (room == null) { + return new ArrayList<>(); + } + + if (sourceType == WiredExtraMovePhysics.SOURCE_ALL_ROOM) { + return new ArrayList<>(room.getFloorItems()); + } + + if (ctx == null) { + return new ArrayList<>(); + } + + return WiredSourceUtil.resolveItemsRaw(ctx, sourceType, null); + } + + private static Collection resolvePhysicsUsers(Room room, WiredContext ctx, int userSource) { + if (room == null) { + return new ArrayList<>(); + } + + if (userSource == WiredExtraMovePhysics.SOURCE_ALL_ROOM) { + return new ArrayList<>(room.getRoomUnits()); + } + + if (ctx == null) { + return new ArrayList<>(); + } + + return WiredSourceUtil.resolveUsersRaw(ctx, userSource); + } + + private static WiredExtraMovePhysics getMovementPhysicsExtra(Room room, HabboItem stackItem) { + THashSet extras = getMovementExtras(room, stackItem); + if (extras == null || extras.isEmpty()) { + return null; + } + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraMovePhysics) { + return (WiredExtraMovePhysics) extra; + } + } + + return null; + } + + private static THashSet getMovementExtras(Room room, HabboItem stackItem) { + if (room == null || stackItem == null || room.getRoomSpecialTypes() == null) { + return null; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(stackItem.getX(), stackItem.getY()); + if (extras == null || extras.isEmpty()) { + return null; + } + + return extras; + } + + private static final class CarryContext { + private final boolean active; + private final List carriedUsers; + private final HashSet carriedUserIds; + + private CarryContext(boolean active, List carriedUsers, HashSet carriedUserIds) { + this.active = active; + this.carriedUsers = carriedUsers; + this.carriedUserIds = carriedUserIds; + } + + private static CarryContext disabled() { + return new CarryContext(false, new ArrayList<>(), new HashSet<>()); + } + } + + private static final class CarriedRoomUnit { + private final RoomUnit roomUnit; + private final RoomTile oldLocation; + private final double oldZ; + private final double heightOffset; + private final int relativeX; + private final int relativeY; + + private CarriedRoomUnit(RoomUnit roomUnit, RoomTile oldLocation, double oldZ, double heightOffset, int relativeX, int relativeY) { + this.roomUnit = roomUnit; + this.oldLocation = oldLocation; + this.oldZ = oldZ; + this.heightOffset = heightOffset; + this.relativeX = relativeX; + this.relativeY = relativeY; + } + } + + private static final class CarriedUnitMove { + private final RoomUnit roomUnit; + private final RoomTile oldLocation; + private final double oldZ; + private final RoomTile destinationTile; + private final double newZ; + + private CarriedUnitMove(RoomUnit roomUnit, RoomTile oldLocation, double oldZ, RoomTile destinationTile, double newZ) { + this.roomUnit = roomUnit; + this.oldLocation = oldLocation; + this.oldZ = oldZ; + this.destinationTile = destinationTile; + this.newZ = newZ; + } + } + + private static final class UserFollowEntry { + private final int roomId; + private final int stackItemId; + private final int movingItemId; + private Double zOverride; + private WiredContext ctx; + private long expiresAt; + private long lastProcessedMoveTimestamp; + + private UserFollowEntry(int roomId, int stackItemId, int movingItemId, Double zOverride, WiredContext ctx) { + this.roomId = roomId; + this.stackItemId = stackItemId; + this.movingItemId = movingItemId; + this.zOverride = zOverride; + this.ctx = ctx; + this.touch(); + } + + private void markProcessed(long moveStatusTimestamp) { + this.lastProcessedMoveTimestamp = moveStatusTimestamp; + this.touch(); + } + + private void touch() { + this.expiresAt = System.currentTimeMillis() + USER_FOLLOWER_TTL_MS; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMovementPhysics.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMovementPhysics.java new file mode 100644 index 00000000..538baa2b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMovementPhysics.java @@ -0,0 +1,56 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitType; +import com.eu.habbo.habbohotel.users.HabboItem; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public final class WiredMovementPhysics { + public static final WiredMovementPhysics NONE = new WiredMovementPhysics(false, Collections.emptySet(), Collections.emptySet(), Collections.emptySet()); + + private final boolean keepAltitude; + private final Set passThroughFurniIds; + private final Set passThroughUserIds; + private final Set blockingFurniIds; + + public WiredMovementPhysics(boolean keepAltitude, Set passThroughFurniIds, Set passThroughUserIds, Set blockingFurniIds) { + this.keepAltitude = keepAltitude; + this.passThroughFurniIds = Collections.unmodifiableSet(new HashSet<>(passThroughFurniIds)); + this.passThroughUserIds = Collections.unmodifiableSet(new HashSet<>(passThroughUserIds)); + this.blockingFurniIds = Collections.unmodifiableSet(new HashSet<>(blockingFurniIds)); + } + + public boolean isKeepAltitude() { + return this.keepAltitude; + } + + public boolean isActive() { + return this.keepAltitude + || !this.passThroughFurniIds.isEmpty() + || !this.passThroughUserIds.isEmpty() + || !this.blockingFurniIds.isEmpty(); + } + + public boolean hasBlockingFurni() { + return !this.blockingFurniIds.isEmpty(); + } + + public boolean shouldIgnoreFurni(HabboItem item) { + return item != null + && this.passThroughFurniIds.contains(item.getId()) + && !this.blockingFurniIds.contains(item.getId()); + } + + public boolean isBlockingFurni(HabboItem item) { + return item != null && this.blockingFurniIds.contains(item.getId()); + } + + public boolean shouldIgnoreUser(RoomUnit roomUnit) { + return roomUnit != null + && roomUnit.getRoomUnitType() == RoomUnitType.USER + && this.passThroughUserIds.contains(roomUnit.getId()); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredRoomDiagnostics.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredRoomDiagnostics.java new file mode 100644 index 00000000..caa7e349 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredRoomDiagnostics.java @@ -0,0 +1,586 @@ +package com.eu.habbo.habbohotel.wired.core; + +import java.util.ArrayList; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; + +/** + * Tracks wired monitor data for a single room. + */ +public final class WiredRoomDiagnostics { + + public enum Type { + EXECUTION_CAP, + DELAYED_EVENTS_CAP, + EXECUTOR_OVERLOAD, + MARKED_AS_HEAVY, + KILLED, + RECURSION_TIMEOUT + } + + public enum Severity { + WARNING, + ERROR + } + + public static final class LogEntry { + private final Type type; + private final Severity severity; + private int count; + private long firstOccurredAtMs; + private long lastOccurredAtMs; + private String latestReason; + private String latestSourceLabel; + private int latestSourceId; + + private LogEntry(Type type, Severity severity) { + this.type = type; + this.severity = severity; + } + + private void record(long now, String reason, String sourceLabel, int sourceId) { + if (this.count <= 0) { + this.firstOccurredAtMs = now; + } + + this.count++; + this.lastOccurredAtMs = now; + this.latestReason = sanitizeReason(reason); + this.latestSourceLabel = sanitizeSourceLabel(sourceLabel); + this.latestSourceId = Math.max(0, sourceId); + } + + public Type getType() { + return type; + } + + public Severity getSeverity() { + return severity; + } + + public int getCount() { + return count; + } + + public long getFirstOccurredAtMs() { + return firstOccurredAtMs; + } + + public long getLastOccurredAtMs() { + return lastOccurredAtMs; + } + + public String getLatestReason() { + return latestReason; + } + + public String getLatestSourceLabel() { + return latestSourceLabel; + } + + public int getLatestSourceId() { + return latestSourceId; + } + } + + public static final class Snapshot { + private final int usageCurrentWindow; + private final int usageLimitPerWindow; + private final boolean heavy; + private final int delayedEventsPending; + private final int delayedEventsLimit; + private final int averageExecutionMs; + private final int peakExecutionMs; + private final int recursionDepthCurrent; + private final int recursionDepthLimit; + private final int killedRemainingSeconds; + private final int usageWindowMs; + private final int overloadAverageThresholdMs; + private final int overloadPeakThresholdMs; + private final int heavyUsageThresholdPercent; + private final int heavyConsecutiveWindowsThreshold; + private final int overloadConsecutiveWindowsThreshold; + private final int heavyDelayedThresholdPercent; + private final List logs; + private final List history; + + public Snapshot(int usageCurrentWindow, int usageLimitPerWindow, boolean heavy, int delayedEventsPending, + int delayedEventsLimit, int averageExecutionMs, int peakExecutionMs, + int recursionDepthCurrent, int recursionDepthLimit, int killedRemainingSeconds, + int usageWindowMs, int overloadAverageThresholdMs, int overloadPeakThresholdMs, + int heavyUsageThresholdPercent, int heavyConsecutiveWindowsThreshold, + int overloadConsecutiveWindowsThreshold, int heavyDelayedThresholdPercent, + List logs, List history) { + this.usageCurrentWindow = usageCurrentWindow; + this.usageLimitPerWindow = usageLimitPerWindow; + this.heavy = heavy; + this.delayedEventsPending = delayedEventsPending; + this.delayedEventsLimit = delayedEventsLimit; + this.averageExecutionMs = averageExecutionMs; + this.peakExecutionMs = peakExecutionMs; + this.recursionDepthCurrent = recursionDepthCurrent; + this.recursionDepthLimit = recursionDepthLimit; + this.killedRemainingSeconds = killedRemainingSeconds; + this.usageWindowMs = usageWindowMs; + this.overloadAverageThresholdMs = overloadAverageThresholdMs; + this.overloadPeakThresholdMs = overloadPeakThresholdMs; + this.heavyUsageThresholdPercent = heavyUsageThresholdPercent; + this.heavyConsecutiveWindowsThreshold = heavyConsecutiveWindowsThreshold; + this.overloadConsecutiveWindowsThreshold = overloadConsecutiveWindowsThreshold; + this.heavyDelayedThresholdPercent = heavyDelayedThresholdPercent; + this.logs = Collections.unmodifiableList(logs); + this.history = Collections.unmodifiableList(history); + } + + public int getUsageCurrentWindow() { + return usageCurrentWindow; + } + + public int getUsageLimitPerWindow() { + return usageLimitPerWindow; + } + + public boolean isHeavy() { + return heavy; + } + + public int getDelayedEventsPending() { + return delayedEventsPending; + } + + public int getDelayedEventsLimit() { + return delayedEventsLimit; + } + + public int getAverageExecutionMs() { + return averageExecutionMs; + } + + public int getPeakExecutionMs() { + return peakExecutionMs; + } + + public int getRecursionDepthCurrent() { + return recursionDepthCurrent; + } + + public int getRecursionDepthLimit() { + return recursionDepthLimit; + } + + public int getKilledRemainingSeconds() { + return killedRemainingSeconds; + } + + public int getUsageWindowMs() { + return usageWindowMs; + } + + public int getOverloadAverageThresholdMs() { + return overloadAverageThresholdMs; + } + + public int getOverloadPeakThresholdMs() { + return overloadPeakThresholdMs; + } + + public int getHeavyUsageThresholdPercent() { + return heavyUsageThresholdPercent; + } + + public int getHeavyConsecutiveWindowsThreshold() { + return heavyConsecutiveWindowsThreshold; + } + + public int getOverloadConsecutiveWindowsThreshold() { + return overloadConsecutiveWindowsThreshold; + } + + public int getHeavyDelayedThresholdPercent() { + return heavyDelayedThresholdPercent; + } + + public List getLogs() { + return logs; + } + + public List getHistory() { + return history; + } + } + + public static final class HistoryEntry { + private final Type type; + private final Severity severity; + private final long occurredAtMs; + private final String reason; + private final String sourceLabel; + private final int sourceId; + + public HistoryEntry(Type type, Severity severity, long occurredAtMs, String reason, String sourceLabel, int sourceId) { + this.type = type; + this.severity = severity; + this.occurredAtMs = occurredAtMs; + this.reason = sanitizeReason(reason); + this.sourceLabel = sanitizeSourceLabel(sourceLabel); + this.sourceId = Math.max(0, sourceId); + } + + public Type getType() { + return type; + } + + public Severity getSeverity() { + return severity; + } + + public long getOccurredAtMs() { + return occurredAtMs; + } + + public String getReason() { + return reason; + } + + public String getSourceLabel() { + return sourceLabel; + } + + public int getSourceId() { + return sourceId; + } + } + + private final int usageWindowMs; + private final int usageLimitPerWindow; + private final int delayedEventsLimit; + private final int overloadAverageThresholdMs; + private final int overloadPeakThresholdMs; + private final int heavyUsageThresholdPercent; + private final int heavyConsecutiveWindowsThreshold; + private final int overloadConsecutiveWindowsThreshold; + private final int heavyDelayedThresholdPercent; + private final EnumMap logs; + private final ArrayDeque history; + private final int maxHistoryEntries; + + private long windowStartedAt; + private int usageCurrentWindow; + private int delayedEventsPending; + private long totalExecutionMsCurrentWindow; + private int executionSamplesCurrentWindow; + private int averageExecutionMs; + private int peakExecutionMs; + private int consecutiveHeavyWindows; + private int consecutiveOverloadWindows; + private boolean heavy; + private String peakExecutionSourceLabel; + private int peakExecutionSourceId; + private String peakExecutionReason; + + public WiredRoomDiagnostics(int usageWindowMs, int usageLimitPerWindow, int delayedEventsLimit, + int overloadAverageThresholdMs, int overloadPeakThresholdMs, + int heavyUsageThresholdPercent, int heavyConsecutiveWindowsThreshold) { + this(usageWindowMs, usageLimitPerWindow, delayedEventsLimit, overloadAverageThresholdMs, overloadPeakThresholdMs, + heavyUsageThresholdPercent, heavyConsecutiveWindowsThreshold, 2, 60, 200); + } + + public WiredRoomDiagnostics(int usageWindowMs, int usageLimitPerWindow, int delayedEventsLimit, + int overloadAverageThresholdMs, int overloadPeakThresholdMs, + int heavyUsageThresholdPercent, int heavyConsecutiveWindowsThreshold, + int overloadConsecutiveWindowsThreshold, int heavyDelayedThresholdPercent, + int maxHistoryEntries) { + this.usageWindowMs = Math.max(250, usageWindowMs); + this.usageLimitPerWindow = Math.max(1, usageLimitPerWindow); + this.delayedEventsLimit = Math.max(1, delayedEventsLimit); + this.overloadAverageThresholdMs = Math.max(1, overloadAverageThresholdMs); + this.overloadPeakThresholdMs = Math.max(this.overloadAverageThresholdMs, overloadPeakThresholdMs); + this.heavyUsageThresholdPercent = Math.max(1, Math.min(100, heavyUsageThresholdPercent)); + this.heavyConsecutiveWindowsThreshold = Math.max(1, heavyConsecutiveWindowsThreshold); + this.overloadConsecutiveWindowsThreshold = Math.max(1, overloadConsecutiveWindowsThreshold); + this.heavyDelayedThresholdPercent = Math.max(1, Math.min(100, heavyDelayedThresholdPercent)); + this.maxHistoryEntries = Math.max(10, maxHistoryEntries); + this.logs = new EnumMap<>(Type.class); + this.history = new ArrayDeque<>(this.maxHistoryEntries); + + for (Type type : Type.values()) { + this.logs.put(type, new LogEntry(type, defaultSeverity(type))); + } + } + + public synchronized boolean tryConsumeExecutionBudget(int estimatedCost, long now, String sourceLabel, int sourceId, String reason) { + rollWindow(now); + + int normalizedCost = Math.max(0, estimatedCost); + if ((this.usageCurrentWindow + normalizedCost) > this.usageLimitPerWindow) { + record(Type.EXECUTION_CAP, now, + buildExecutionCapReason(normalizedCost, reason), + sourceLabel, + sourceId); + return false; + } + + this.usageCurrentWindow += normalizedCost; + return true; + } + + public synchronized boolean tryScheduleDelayedEvent(long now, String sourceLabel, int sourceId, String reason) { + rollWindow(now); + + if ((this.delayedEventsPending + 1) > this.delayedEventsLimit) { + record(Type.DELAYED_EVENTS_CAP, now, + buildDelayedCapReason(reason), + sourceLabel, + sourceId); + return false; + } + + this.delayedEventsPending++; + return true; + } + + public synchronized void completeDelayedEvent() { + if (this.delayedEventsPending > 0) { + this.delayedEventsPending--; + } + } + + public synchronized void recordExecution(long elapsedMs, long now, String sourceLabel, int sourceId, String reason) { + rollWindow(now); + + int normalizedElapsed = (int) Math.max(0L, elapsedMs); + + this.totalExecutionMsCurrentWindow += normalizedElapsed; + this.executionSamplesCurrentWindow++; + this.averageExecutionMs = (int) Math.round(this.totalExecutionMsCurrentWindow / (double) this.executionSamplesCurrentWindow); + + if (normalizedElapsed >= this.peakExecutionMs) { + this.peakExecutionMs = normalizedElapsed; + this.peakExecutionSourceLabel = sanitizeSourceLabel(sourceLabel); + this.peakExecutionSourceId = Math.max(0, sourceId); + this.peakExecutionReason = sanitizeReason(reason); + } + } + + public synchronized void recordKilled(long now, String reason, String sourceLabel, int sourceId) { + rollWindow(now); + record(Type.KILLED, now, reason, sourceLabel, sourceId); + } + + public synchronized void recordRecursionTimeout(long now, String reason, String sourceLabel, int sourceId) { + rollWindow(now); + record(Type.RECURSION_TIMEOUT, now, reason, sourceLabel, sourceId); + } + + public synchronized void clearLogs() { + for (Type type : Type.values()) { + LogEntry entry = this.logs.get(type); + + if (entry == null) { + continue; + } + + entry.count = 0; + entry.firstOccurredAtMs = 0L; + entry.lastOccurredAtMs = 0L; + entry.latestReason = ""; + entry.latestSourceLabel = ""; + entry.latestSourceId = 0; + } + + this.history.clear(); + } + + public synchronized Snapshot snapshot(int recursionDepthCurrent, int recursionDepthLimit, long killedUntilMs, long now) { + rollWindow(now); + + List logEntries = new ArrayList<>(Type.values().length); + List historyEntries = new ArrayList<>(this.history.size()); + + for (Type type : Type.values()) { + LogEntry source = this.logs.get(type); + LogEntry copy = new LogEntry(source.getType(), source.getSeverity()); + + copy.count = source.getCount(); + copy.firstOccurredAtMs = source.getFirstOccurredAtMs(); + copy.lastOccurredAtMs = source.getLastOccurredAtMs(); + copy.latestReason = source.getLatestReason(); + copy.latestSourceLabel = source.getLatestSourceLabel(); + copy.latestSourceId = source.getLatestSourceId(); + + logEntries.add(copy); + } + + historyEntries.addAll(this.history); + + int killedRemainingSeconds = 0; + + if (killedUntilMs > now) { + killedRemainingSeconds = (int) Math.max(0L, Math.ceil((killedUntilMs - now) / 1000D)); + } + + return new Snapshot( + this.usageCurrentWindow, + this.usageLimitPerWindow, + this.heavy, + this.delayedEventsPending, + this.delayedEventsLimit, + this.averageExecutionMs, + this.peakExecutionMs, + recursionDepthCurrent, + recursionDepthLimit, + killedRemainingSeconds, + this.usageWindowMs, + this.overloadAverageThresholdMs, + this.overloadPeakThresholdMs, + this.heavyUsageThresholdPercent, + this.heavyConsecutiveWindowsThreshold, + this.overloadConsecutiveWindowsThreshold, + this.heavyDelayedThresholdPercent, + logEntries, + historyEntries + ); + } + + private void rollWindow(long now) { + if (this.windowStartedAt <= 0L) { + this.windowStartedAt = now; + return; + } + + while ((now - this.windowStartedAt) >= this.usageWindowMs) { + evaluateWindow(this.windowStartedAt + this.usageWindowMs); + this.windowStartedAt += this.usageWindowMs; + this.usageCurrentWindow = 0; + this.totalExecutionMsCurrentWindow = 0L; + this.executionSamplesCurrentWindow = 0; + this.averageExecutionMs = 0; + this.peakExecutionMs = 0; + this.peakExecutionSourceLabel = null; + this.peakExecutionSourceId = 0; + this.peakExecutionReason = null; + } + } + + private void evaluateWindow(long now) { + int usagePercent = (int) Math.round((this.usageCurrentWindow * 100D) / this.usageLimitPerWindow); + int delayedPercent = (int) Math.round((this.delayedEventsPending * 100D) / this.delayedEventsLimit); + boolean overloadWindow = (this.executionSamplesCurrentWindow > 0) + && ((this.averageExecutionMs >= this.overloadAverageThresholdMs) || (this.peakExecutionMs >= this.overloadPeakThresholdMs)); + boolean heavyWindow = (usagePercent >= this.heavyUsageThresholdPercent) + || (delayedPercent >= this.heavyDelayedThresholdPercent) + || overloadWindow; + + if (overloadWindow) { + this.consecutiveOverloadWindows++; + + if (this.consecutiveOverloadWindows >= this.overloadConsecutiveWindowsThreshold) { + record(Type.EXECUTOR_OVERLOAD, now, + buildExecutorOverloadReason(), + this.peakExecutionSourceLabel, + this.peakExecutionSourceId); + } + } else { + this.consecutiveOverloadWindows = 0; + } + + if (heavyWindow) { + this.consecutiveHeavyWindows++; + + if (!this.heavy && (this.consecutiveHeavyWindows >= this.heavyConsecutiveWindowsThreshold)) { + this.heavy = true; + record(Type.MARKED_AS_HEAVY, now, + buildHeavyReason(usagePercent, delayedPercent, overloadWindow), + overloadWindow ? this.peakExecutionSourceLabel : null, + overloadWindow ? this.peakExecutionSourceId : 0); + } + + return; + } + + this.consecutiveHeavyWindows = 0; + this.heavy = false; + } + + private void record(Type type, long now, String reason, String sourceLabel, int sourceId) { + LogEntry entry = this.logs.get(type); + if (entry != null) { + entry.record(now, reason, sourceLabel, sourceId); + this.history.addFirst(new HistoryEntry(type, entry.getSeverity(), now, reason, sourceLabel, sourceId)); + + while (this.history.size() > this.maxHistoryEntries) { + this.history.removeLast(); + } + } + } + + private String buildExecutionCapReason(int normalizedCost, String reason) { + return joinReason( + reason, + String.format("Estimated stack cost %d would exceed usage budget %d/%d in %dms window", + normalizedCost, + this.usageCurrentWindow, + this.usageLimitPerWindow, + this.usageWindowMs) + ); + } + + private String buildDelayedCapReason(String reason) { + return joinReason( + reason, + String.format("Pending delayed events would exceed queue %d/%d", + this.delayedEventsPending, + this.delayedEventsLimit) + ); + } + + private String buildExecutorOverloadReason() { + return joinReason( + this.peakExecutionReason, + String.format("Average execution %dms (limit %dms), peak %dms (limit %dms) across %d execution(s) in %dms window", + this.averageExecutionMs, + this.overloadAverageThresholdMs, + this.peakExecutionMs, + this.overloadPeakThresholdMs, + this.executionSamplesCurrentWindow, + this.usageWindowMs) + ); + } + + private String buildHeavyReason(int usagePercent, int delayedPercent, boolean overloadWindow) { + return String.format( + "Room stayed above heavy thresholds for %d consecutive window(s): usage %d%%/%d%%, delayed %d%%/%d%%, overload %s", + this.consecutiveHeavyWindows, + usagePercent, + this.heavyUsageThresholdPercent, + delayedPercent, + this.heavyDelayedThresholdPercent, + overloadWindow ? "yes" : "no" + ); + } + + private static String joinReason(String primary, String fallback) { + String cleanPrimary = sanitizeReason(primary); + String cleanFallback = sanitizeReason(fallback); + + if (cleanPrimary.isEmpty()) return cleanFallback; + if (cleanFallback.isEmpty()) return cleanPrimary; + if (cleanPrimary.equals(cleanFallback)) return cleanPrimary; + + return cleanPrimary + ". " + cleanFallback; + } + + private static String sanitizeReason(String reason) { + return (reason == null) ? "" : reason.trim(); + } + + private static String sanitizeSourceLabel(String sourceLabel) { + return (sourceLabel == null) ? "" : sourceLabel.trim(); + } + + private Severity defaultSeverity(Type type) { + return (type == Type.MARKED_AS_HEAVY) ? Severity.WARNING : Severity.ERROR; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSelectionFilterSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSelectionFilterSupport.java new file mode 100644 index 00000000..306e71b5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSelectionFilterSupport.java @@ -0,0 +1,204 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurni; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurniByVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUser; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUsersByVariable; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import gnu.trove.set.hash.THashSet; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +final class WiredSelectionFilterSupport { + private static final ThreadLocal FILTER_DEPTH = ThreadLocal.withInitial(() -> 0); + + private WiredSelectionFilterSupport() { + } + + static void applySelectorFilters(Room room, HabboItem triggerItem, WiredContext ctx) { + if (ctx == null) { + return; + } + + if (ctx.targets().isItemsModifiedBySelector()) { + ctx.targets().setItems(filterItems(room, triggerItem, ctx, ctx.targets().items())); + } + + if (ctx.targets().isUsersModifiedBySelector()) { + ctx.targets().setUsers(filterUsers(room, triggerItem, ctx, ctx.targets().users())); + } + } + + static List filterItems(Room room, HabboItem triggerItem, WiredContext ctx, Iterable values) { + List items = toItemList(values); + + if (items.isEmpty() || shouldBypass(room, triggerItem, ctx)) { + return items; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(triggerItem.getX(), triggerItem.getY()); + if (extras == null || extras.isEmpty()) { + return items; + } + + int furniLimit = Integer.MAX_VALUE; + List variableFilters = new ArrayList<>(); + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraFilterFurni) { + furniLimit = Math.min(furniLimit, ((WiredExtraFilterFurni) extra).getAmount()); + } else if (extra instanceof WiredExtraFilterFurniByVariable) { + variableFilters.add((WiredExtraFilterFurniByVariable) extra); + } + } + + if (furniLimit == Integer.MAX_VALUE && variableFilters.isEmpty()) { + return items; + } + + variableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); + + try (FilterScope ignored = enterScope()) { + Iterable filteredItems = items; + + for (WiredExtraFilterFurniByVariable extra : variableFilters) { + filteredItems = extra.filterItems(room, ctx, filteredItems); + } + + if (furniLimit != Integer.MAX_VALUE) { + filteredItems = limitIterable(filteredItems, furniLimit); + } + + return toItemList(filteredItems); + } + } + + static List filterUsers(Room room, HabboItem triggerItem, WiredContext ctx, Iterable values) { + List users = toUserList(values); + + if (users.isEmpty() || shouldBypass(room, triggerItem, ctx)) { + return users; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(triggerItem.getX(), triggerItem.getY()); + if (extras == null || extras.isEmpty()) { + return users; + } + + int userLimit = Integer.MAX_VALUE; + List variableFilters = new ArrayList<>(); + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraFilterUser) { + userLimit = Math.min(userLimit, ((WiredExtraFilterUser) extra).getAmount()); + } else if (extra instanceof WiredExtraFilterUsersByVariable) { + variableFilters.add((WiredExtraFilterUsersByVariable) extra); + } + } + + if (userLimit == Integer.MAX_VALUE && variableFilters.isEmpty()) { + return users; + } + + variableFilters.sort((left, right) -> Integer.compare(left.getId(), right.getId())); + + try (FilterScope ignored = enterScope()) { + Iterable filteredUsers = users; + + for (WiredExtraFilterUsersByVariable extra : variableFilters) { + filteredUsers = extra.filterUsers(room, ctx, filteredUsers); + } + + if (userLimit != Integer.MAX_VALUE) { + filteredUsers = limitIterable(filteredUsers, userLimit); + } + + return toUserList(filteredUsers); + } + } + + private static boolean shouldBypass(Room room, HabboItem triggerItem, WiredContext ctx) { + return room == null + || triggerItem == null + || ctx == null + || room.getRoomSpecialTypes() == null + || FILTER_DEPTH.get() > 0; + } + + private static FilterScope enterScope() { + FILTER_DEPTH.set(FILTER_DEPTH.get() + 1); + return new FilterScope(); + } + + private static List limitIterable(Iterable values, int limit) { + List result = new ArrayList<>(); + + if (values == null || limit <= 0) { + return result; + } + + for (T value : values) { + if (value != null) { + result.add(value); + } + } + + if (result.size() <= limit) { + return result; + } + + Collections.shuffle(result, Emulator.getRandom()); + return new ArrayList<>(result.subList(0, limit)); + } + + private static List toItemList(Iterable values) { + List result = new ArrayList<>(); + + if (values == null) { + return result; + } + + for (HabboItem item : values) { + if (item != null) { + result.add(item); + } + } + + return result; + } + + private static List toUserList(Iterable values) { + List result = new ArrayList<>(); + + if (values == null) { + return result; + } + + for (RoomUnit unit : values) { + if (unit != null) { + result.add(unit); + } + } + + return result; + } + + private static final class FilterScope implements AutoCloseable { + @Override + public void close() { + int depth = FILTER_DEPTH.get() - 1; + + if (depth <= 0) { + FILTER_DEPTH.remove(); + } else { + FILTER_DEPTH.set(depth); + } + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java index ed65404a..00e41a15 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredSourceUtil.java @@ -1,7 +1,17 @@ package com.eu.habbo.habbohotel.wired.core; +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurni; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurniByVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUser; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUsersByVariable; +import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.api.IWiredEffect; +import gnu.trove.set.hash.THashSet; import java.util.ArrayList; import java.util.Collection; @@ -10,6 +20,7 @@ import java.util.List; public final class WiredSourceUtil { public static final int SOURCE_TRIGGER = 0; + public static final int SOURCE_CLICKED_USER = 11; public static final int SOURCE_SELECTED = 100; public static final int SOURCE_SELECTOR = 200; public static final int SOURCE_SIGNAL = 201; @@ -18,23 +29,19 @@ public final class WiredSourceUtil { } public static List resolveItems(WiredContext ctx, int sourceType, Collection selectedItems) { - switch (sourceType) { - case SOURCE_TRIGGER: - return ctx.sourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); - case SOURCE_SELECTED: - return (selectedItems != null) ? new ArrayList<>(selectedItems) : Collections.emptyList(); - case SOURCE_SELECTOR: - return ctx.targets().isItemsModifiedBySelector() - ? new ArrayList<>(ctx.targets().items()) - : Collections.emptyList(); - case SOURCE_SIGNAL: - if (ctx.eventType() == WiredEvent.Type.SIGNAL_RECEIVED) { - return ctx.sourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); - } - return Collections.emptyList(); - default: - return ctx.sourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); + List resolvedItems = resolveItemsInternal(ctx, sourceType, selectedItems, false); + + if (ctx == null) { + return resolvedItems; } + + return (sourceType == SOURCE_SELECTOR) + ? resolvedItems + : WiredSelectionFilterSupport.filterItems(ctx.room(), ctx.triggerItem(), ctx, resolvedItems); + } + + public static List resolveItemsRaw(WiredContext ctx, int sourceType, Collection selectedItems) { + return resolveItemsInternal(ctx, sourceType, selectedItems, true); } public static List resolveUsers(WiredContext ctx, int sourceType) { @@ -42,14 +49,216 @@ public final class WiredSourceUtil { } public static List resolveUsers(WiredContext ctx, int sourceType, Collection selectedUsers) { + List resolvedUsers = resolveUsersInternal(ctx, sourceType, selectedUsers); + + if (ctx == null) { + return resolvedUsers; + } + + return (sourceType == SOURCE_SELECTOR) + ? resolvedUsers + : WiredSelectionFilterSupport.filterUsers(ctx.room(), ctx.triggerItem(), ctx, resolvedUsers); + } + + public static List resolveUsersRaw(WiredContext ctx, int sourceType) { + return resolveUsersRaw(ctx, sourceType, null); + } + + public static List resolveUsersRaw(WiredContext ctx, int sourceType, Collection selectedUsers) { + return resolveUsersInternal(ctx, sourceType, selectedUsers); + } + + public static boolean isDefaultUserSource(int value) { + switch (value) { + case SOURCE_TRIGGER: + case SOURCE_CLICKED_USER: + case SOURCE_SELECTOR: + case SOURCE_SIGNAL: + return true; + default: + return false; + } + } + + public static boolean isSelectableUserSource(int value) { + return value == SOURCE_SELECTED || isDefaultUserSource(value); + } + + public static List resolveSelectorItems(WiredContext ctx, boolean includeWiredItems) { + if (ctx == null) { + return Collections.emptyList(); + } + + if (!includeWiredItems) { + return resolveItems(ctx, SOURCE_SELECTOR, null); + } + + WiredContext selectorContext = executeSelectors(cloneSelectorContext(ctx, true)); + + if (selectorContext == null || !selectorContext.targets().isItemsModifiedBySelector()) { + return Collections.emptyList(); + } + + return new ArrayList<>(selectorContext.targets().items()); + } + + private static WiredTargets getSelectorTargets(WiredContext ctx) { + if (ctx == null) { + return new WiredTargets(); + } + + if (ctx.targets().isItemsModifiedBySelector() || ctx.targets().isUsersModifiedBySelector()) { + return ctx.targets(); + } + + WiredContext selectorContext = executeSelectors(ctx); + + if (selectorContext == null) { + return ctx.targets(); + } + + if (selectorContext.targets().isItemsModifiedBySelector()) { + ctx.targets().setItems(selectorContext.targets().items()); + } + + if (selectorContext.targets().isUsersModifiedBySelector()) { + ctx.targets().setUsers(selectorContext.targets().users()); + } + + return ctx.targets(); + } + + private static WiredContext executeSelectors(WiredContext originalCtx) { + if (originalCtx == null) { + return null; + } + + Room room = originalCtx.room(); + HabboItem triggerItem = originalCtx.triggerItem(); + + if (room == null || triggerItem == null || room.getRoomSpecialTypes() == null) { + return null; + } + + WiredContext selectorCtx = new WiredContext( + originalCtx.event(), + triggerItem, + originalCtx.stack(), + originalCtx.services(), + new WiredState(100), + originalCtx.legacySettings() + ); + selectorCtx.setIncludeWiredSelectorItems(originalCtx.includeWiredSelectorItems()); + + List selectorEffects = getOrderedSelectorEffects(originalCtx, room, triggerItem); + + for (InteractionWiredEffect effect : selectorEffects) { + if (effect.requiresActor() && !selectorCtx.hasActor()) { + continue; + } + + try { + selectorCtx.state().step(); + effect.execute(selectorCtx); + } catch (Exception ignored) { + } + } + + applySelectionFilterExtras(room, triggerItem, selectorCtx); + + return selectorCtx; + } + + private static WiredContext cloneSelectorContext(WiredContext originalCtx, boolean includeWiredItems) { + if (originalCtx == null) { + return null; + } + + WiredContext selectorCtx = new WiredContext( + originalCtx.event(), + originalCtx.triggerItem(), + originalCtx.stack(), + originalCtx.services(), + new WiredState(100), + originalCtx.legacySettings() + ); + selectorCtx.setIncludeWiredSelectorItems(includeWiredItems); + return selectorCtx; + } + + private static List getOrderedSelectorEffects(WiredContext originalCtx, Room room, HabboItem triggerItem) { + List selectorEffects = new ArrayList<>(); + + if (originalCtx != null && originalCtx.hasStack()) { + for (IWiredEffect effect : originalCtx.stack().effects()) { + if (effect instanceof InteractionWiredEffect && effect.isSelector()) { + selectorEffects.add((InteractionWiredEffect) effect); + } + } + + if (!selectorEffects.isEmpty()) { + return selectorEffects; + } + } + + THashSet roomEffects = room.getRoomSpecialTypes().getEffects(triggerItem.getX(), triggerItem.getY()); + for (InteractionWiredEffect effect : WiredExecutionOrderUtil.sort(roomEffects)) { + if (effect != null && effect.isSelector()) { + selectorEffects.add(effect); + } + } + + return selectorEffects; + } + + private static void applySelectionFilterExtras(Room room, HabboItem triggerItem, WiredContext selectorCtx) { + WiredSelectionFilterSupport.applySelectorFilters(room, triggerItem, selectorCtx); + } + + private static List resolveItemsInternal(WiredContext ctx, int sourceType, Collection selectedItems, boolean allowTriggerItemFallback) { + if (ctx == null) { + return Collections.emptyList(); + } + + switch (sourceType) { + case SOURCE_TRIGGER: + return resolveTriggerItems(ctx, allowTriggerItemFallback); + case SOURCE_SELECTED: + return (selectedItems != null) ? new ArrayList<>(selectedItems) : Collections.emptyList(); + case SOURCE_SELECTOR: + WiredTargets itemTargets = getSelectorTargets(ctx); + return itemTargets.isItemsModifiedBySelector() + ? new ArrayList<>(itemTargets.items()) + : Collections.emptyList(); + case SOURCE_SIGNAL: + if (ctx.eventType() == WiredEvent.Type.SIGNAL_RECEIVED) { + return ctx.sourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); + } + return Collections.emptyList(); + default: + return resolveTriggerItems(ctx, allowTriggerItemFallback); + } + } + + private static List resolveUsersInternal(WiredContext ctx, int sourceType, Collection selectedUsers) { + if (ctx == null) { + return Collections.emptyList(); + } + switch (sourceType) { case SOURCE_TRIGGER: return ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList()); + case SOURCE_CLICKED_USER: + if (ctx.eventType() == WiredEvent.Type.USER_CLICKS_USER) { + return ctx.event().getTargetUnit().map(Collections::singletonList).orElse(Collections.emptyList()); + } + return Collections.emptyList(); case SOURCE_SELECTED: return (selectedUsers != null) ? new ArrayList<>(selectedUsers) : Collections.emptyList(); case SOURCE_SELECTOR: - return ctx.targets().isUsersModifiedBySelector() - ? new ArrayList<>(ctx.targets().users()) + WiredTargets userTargets = getSelectorTargets(ctx); + return userTargets.isUsersModifiedBySelector() + ? new ArrayList<>(userTargets.users()) : Collections.emptyList(); case SOURCE_SIGNAL: if (ctx.eventType() == WiredEvent.Type.SIGNAL_RECEIVED) { @@ -60,4 +269,20 @@ public final class WiredSourceUtil { return ctx.actor().map(Collections::singletonList).orElse(Collections.emptyList()); } } + + private static List resolveTriggerItems(WiredContext ctx, boolean allowTriggerItemFallback) { + if (ctx == null) { + return Collections.emptyList(); + } + + if (ctx.sourceItem().isPresent()) { + return Collections.singletonList(ctx.sourceItem().get()); + } + + if (allowTriggerItemFallback && ctx.triggerItem() != null) { + return Collections.singletonList(ctx.triggerItem()); + } + + return Collections.emptyList(); + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java new file mode 100644 index 00000000..cb73d427 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextInputCaptureSupport.java @@ -0,0 +1,246 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextInputVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerHabboSaysKeyword; +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.habbohotel.wired.api.WiredStack; +import gnu.trove.set.hash.THashSet; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class WiredTextInputCaptureSupport { + private static final int MATCH_CONTAINS = 0; + private static final int MATCH_EXACT = 1; + private static final int MATCH_ALL_WORDS = 2; + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("#([^#]+)#"); + + private WiredTextInputCaptureSupport() { + } + + public static CaptureResult resolve(WiredStack stack, WiredEvent event) { + if (stack == null || event == null || !(stack.triggerItem() instanceof WiredTriggerHabboSaysKeyword)) { + return CaptureResult.noMatch(); + } + + WiredTriggerHabboSaysKeyword trigger = (WiredTriggerHabboSaysKeyword) stack.triggerItem(); + Room room = event.getRoom(); + RoomUnit actor = event.getActor().orElse(null); + String text = event.getText().orElse(null); + + if (room == null || actor == null || text == null) { + return CaptureResult.noMatch(); + } + + List capturers = getCapturers(room, trigger); + if (capturers.isEmpty()) { + return trigger.matches(stack.triggerItem(), event) ? CaptureResult.matched(new LinkedHashMap<>()) : CaptureResult.noMatch(); + } + + if (trigger.isOwnerOnly()) { + Habbo habbo = room.getHabbo(actor); + if (habbo == null || room.getOwnerId() != habbo.getHabboInfo().getId()) { + return CaptureResult.noMatch(); + } + } + + LinkedHashMap capturersByName = new LinkedHashMap<>(); + for (WiredExtraTextInputVariable capturer : capturers) { + if (capturer == null || capturer.getCapturerName() == null || capturer.getCapturerName().trim().isEmpty()) { + continue; + } + + capturersByName.put(capturer.getCapturerName().trim().toLowerCase(), capturer); + } + + if (capturersByName.isEmpty()) { + return trigger.matches(stack.triggerItem(), event) ? CaptureResult.matched(new LinkedHashMap<>()) : CaptureResult.noMatch(); + } + + MatchResult matchResult = matchTemplate(trigger, text, capturersByName); + if (!matchResult.matches) { + return CaptureResult.noMatch(); + } + + LinkedHashMap capturedValues = new LinkedHashMap<>(); + for (Map.Entry capture : matchResult.captures.entrySet()) { + WiredExtraTextInputVariable capturer = capturersByName.get(capture.getKey()); + if (capturer == null) { + continue; + } + + Integer resolvedValue = capturer.resolveCapturedValue(room, capture.getValue()); + if (resolvedValue == null) { + return CaptureResult.noMatch(); + } + + capturedValues.put(capturer.getVariableItemId(), resolvedValue); + } + + return CaptureResult.matched(capturedValues); + } + + private static List getCapturers(Room room, WiredTriggerHabboSaysKeyword trigger) { + List capturers = new ArrayList<>(); + + if (room == null || trigger == null || room.getRoomSpecialTypes() == null) { + return capturers; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(trigger.getX(), trigger.getY()); + if (extras == null || extras.isEmpty()) { + return capturers; + } + + for (InteractionWiredExtra extra : WiredExecutionOrderUtil.sort(extras)) { + if (extra instanceof WiredExtraTextInputVariable) { + capturers.add((WiredExtraTextInputVariable) extra); + } + } + + return capturers; + } + + private static MatchResult matchTemplate(WiredTriggerHabboSaysKeyword trigger, String rawText, Map capturersByName) { + String text = rawText != null ? rawText.trim() : ""; + String template = trigger.getKey() != null ? trigger.getKey().trim() : ""; + + if (trigger.getMatchMode() == MATCH_ALL_WORDS && template.isEmpty()) { + if (capturersByName.size() != 1 || text.isEmpty()) { + return MatchResult.noMatch(); + } + + String placeholderName = capturersByName.keySet().iterator().next(); + LinkedHashMap captures = new LinkedHashMap<>(); + captures.put(placeholderName, text); + return MatchResult.matched(captures); + } + + TemplatePattern pattern = buildPattern(template); + if (pattern == null) { + return MatchResult.noMatch(); + } + + Matcher matcher = pattern.pattern.matcher(text); + boolean matches = (trigger.getMatchMode() == MATCH_CONTAINS) ? matcher.find() : matcher.matches(); + if (!matches) { + return MatchResult.noMatch(); + } + + LinkedHashMap captures = new LinkedHashMap<>(); + for (int index = 0; index < pattern.placeholderNames.size(); index++) { + String placeholderName = pattern.placeholderNames.get(index); + if (!capturersByName.containsKey(placeholderName)) { + continue; + } + + String capturedValue = matcher.group(index + 1); + captures.put(placeholderName, capturedValue != null ? capturedValue.trim() : ""); + } + + return MatchResult.matched(captures); + } + + private static TemplatePattern buildPattern(String template) { + if (template == null || template.isEmpty()) { + return null; + } + + Matcher matcher = PLACEHOLDER_PATTERN.matcher(template); + StringBuilder regex = new StringBuilder(); + List placeholderNames = new ArrayList<>(); + int cursor = 0; + + while (matcher.find()) { + regex.append(Pattern.quote(template.substring(cursor, matcher.start()))); + regex.append("(.+?)"); + + String placeholderName = matcher.group(1) != null ? matcher.group(1).trim().toLowerCase() : ""; + placeholderNames.add(placeholderName); + cursor = matcher.end(); + } + + regex.append(Pattern.quote(template.substring(cursor))); + + if (placeholderNames.isEmpty()) { + regex = new StringBuilder(Pattern.quote(template)); + } + + return new TemplatePattern(Pattern.compile(regex.toString(), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE), placeholderNames); + } + + public static void applyToContext(WiredContext ctx, Room room, CaptureResult captureResult) { + if (ctx == null || room == null || captureResult == null || !captureResult.matches || captureResult.capturedValues.isEmpty()) { + return; + } + + ctx.forkContextVariables(); + + for (Map.Entry entry : captureResult.capturedValues.entrySet()) { + if (entry == null || entry.getKey() == null || entry.getKey() <= 0) { + continue; + } + + if (!WiredContextVariableSupport.updateVariableValue(ctx, room, entry.getKey(), entry.getValue())) { + WiredContextVariableSupport.assignVariable(ctx, room, entry.getKey(), entry.getValue(), false); + } + } + } + + public static final class CaptureResult { + private final boolean matches; + private final LinkedHashMap capturedValues; + + private CaptureResult(boolean matches, LinkedHashMap capturedValues) { + this.matches = matches; + this.capturedValues = capturedValues; + } + + public boolean matches() { + return this.matches; + } + + public static CaptureResult matched(LinkedHashMap capturedValues) { + return new CaptureResult(true, capturedValues); + } + + public static CaptureResult noMatch() { + return new CaptureResult(false, new LinkedHashMap<>()); + } + } + + private static final class MatchResult { + private final boolean matches; + private final LinkedHashMap captures; + + private MatchResult(boolean matches, LinkedHashMap captures) { + this.matches = matches; + this.captures = captures; + } + + private static MatchResult matched(LinkedHashMap captures) { + return new MatchResult(true, captures); + } + + private static MatchResult noMatch() { + return new MatchResult(false, new LinkedHashMap<>()); + } + } + + private static final class TemplatePattern { + private final Pattern pattern; + private final List placeholderNames; + + private TemplatePattern(Pattern pattern, List placeholderNames) { + this.pattern = pattern; + this.placeholderNames = placeholderNames; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java new file mode 100644 index 00000000..d1db42ab --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTextPlaceholderUtil.java @@ -0,0 +1,516 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.bots.Bot; +import com.eu.habbo.habbohotel.games.Game; +import com.eu.habbo.habbohotel.games.GamePlayer; +import com.eu.habbo.habbohotel.games.GameTeam; +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.games.battlebanzai.BattleBanzaiGame; +import com.eu.habbo.habbohotel.games.freeze.FreezeGame; +import com.eu.habbo.habbohotel.games.wired.WiredGame; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextOutputFurniName; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextOutputUsername; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraTextOutputVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraUserVariable; +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.pets.Pet; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitType; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.util.HotelDateTimeUtil; +import gnu.trove.set.hash.THashSet; + +import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; + +public final class WiredTextPlaceholderUtil { + private WiredTextPlaceholderUtil() { + } + + public static String applyUsernamePlaceholders(WiredContext ctx, String text) { + if (ctx == null || text == null || text.isEmpty()) { + return text; + } + + Room room = ctx.room(); + HabboItem triggerItem = ctx.triggerItem(); + + if (room == null || triggerItem == null || room.getRoomSpecialTypes() == null) { + return text; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(triggerItem.getX(), triggerItem.getY()); + if (extras == null || extras.isEmpty()) { + return text; + } + + String resolvedText = text; + + for (InteractionWiredExtra extra : WiredExecutionOrderUtil.sort(extras)) { + if (extra instanceof WiredExtraTextOutputUsername) { + WiredExtraTextOutputUsername usernameExtra = (WiredExtraTextOutputUsername) extra; + String placeholderToken = usernameExtra.getPlaceholderToken(); + + if (!placeholderToken.isEmpty() && resolvedText.contains(placeholderToken)) { + resolvedText = resolvedText.replace(placeholderToken, buildUsernameReplacement(ctx, usernameExtra)); + } + + continue; + } + + if (extra instanceof WiredExtraTextOutputFurniName) { + WiredExtraTextOutputFurniName furniExtra = (WiredExtraTextOutputFurniName) extra; + String placeholderToken = furniExtra.getPlaceholderToken(); + + if (!placeholderToken.isEmpty() && resolvedText.contains(placeholderToken)) { + resolvedText = resolvedText.replace(placeholderToken, buildFurniNameReplacement(ctx, furniExtra)); + } + + continue; + } + + if (extra instanceof WiredExtraTextOutputVariable) { + WiredExtraTextOutputVariable variableExtra = (WiredExtraTextOutputVariable) extra; + String placeholderToken = variableExtra.getPlaceholderToken(); + + if (!placeholderToken.isEmpty() && resolvedText.contains(placeholderToken)) { + resolvedText = resolvedText.replace(placeholderToken, buildVariableReplacement(ctx, variableExtra)); + } + } + } + + return resolvedText; + } + + public static boolean requiresActor(Room room, HabboItem stackItem) { + if (room == null || stackItem == null || room.getRoomSpecialTypes() == null) { + return false; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(stackItem.getX(), stackItem.getY()); + if (extras == null || extras.isEmpty()) { + return false; + } + + for (InteractionWiredExtra extra : extras) { + if (extra instanceof WiredExtraTextOutputUsername) { + int userSource = ((WiredExtraTextOutputUsername) extra).getUserSource(); + if (userSource == WiredSourceUtil.SOURCE_TRIGGER || userSource == WiredSourceUtil.SOURCE_CLICKED_USER) { + return true; + } + } + + if (extra instanceof WiredExtraTextOutputVariable && ((WiredExtraTextOutputVariable) extra).requiresActor()) { + return true; + } + } + + return false; + } + + private static String buildUsernameReplacement(WiredContext ctx, WiredExtraTextOutputUsername extra) { + List users = WiredSourceUtil.resolveUsers(ctx, extra.getUserSource()); + if (users.isEmpty()) { + return ""; + } + + LinkedHashSet seenUserIds = new LinkedHashSet<>(); + List usernames = new ArrayList<>(); + + for (RoomUnit unit : users) { + if ((unit == null) || !seenUserIds.add(unit.getId())) { + continue; + } + + String roomUnitName = getRoomUnitName(ctx.room(), unit); + if (roomUnitName == null || roomUnitName.trim().isEmpty()) { + continue; + } + + usernames.add(roomUnitName); + } + + if (usernames.isEmpty()) { + return ""; + } + + if (extra.getPlaceholderType() == WiredExtraTextOutputUsername.TYPE_MULTIPLE) { + return String.join(extra.getDelimiter(), usernames); + } + + return usernames.get(0); + } + + private static String buildFurniNameReplacement(WiredContext ctx, WiredExtraTextOutputFurniName extra) { + List items; + if (extra.getFurniSource() == WiredSourceUtil.SOURCE_SELECTOR) { + items = WiredSourceUtil.resolveSelectorItems(ctx, true); + } else { + items = WiredSourceUtil.resolveItems(ctx, extra.getFurniSource(), extra.getItems()); + } + + if (items.isEmpty()) { + return ""; + } + + LinkedHashSet seenItemIds = new LinkedHashSet<>(); + List furniNames = new ArrayList<>(); + + for (HabboItem item : items) { + if ((item == null) || (item.getBaseItem() == null) || !seenItemIds.add(item.getId())) { + continue; + } + + String furniName = item.getBaseItem().getFullName(); + if (furniName == null || furniName.trim().isEmpty()) { + furniName = item.getBaseItem().getName(); + } + + if (furniName == null || furniName.trim().isEmpty()) { + continue; + } + + furniNames.add(furniName); + } + + if (furniNames.isEmpty()) { + return ""; + } + + if (extra.getPlaceholderType() == WiredExtraTextOutputFurniName.TYPE_MULTIPLE) { + return String.join(extra.getDelimiter(), furniNames); + } + + return furniNames.get(0); + } + + private static String buildVariableReplacement(WiredContext ctx, WiredExtraTextOutputVariable extra) { + List values = switch (extra.getTargetType()) { + case WiredExtraTextOutputVariable.TARGET_FURNI -> collectFurniVariableValues(ctx, extra); + case WiredExtraTextOutputVariable.TARGET_CONTEXT -> collectContextVariableValues(ctx, extra); + case WiredExtraTextOutputVariable.TARGET_ROOM -> collectRoomVariableValues(ctx, extra); + default -> collectUserVariableValues(ctx, extra); + }; + + if (values.isEmpty()) { + return ""; + } + + if (extra.getPlaceholderType() == WiredExtraTextOutputVariable.TYPE_MULTIPLE) { + return String.join(extra.getDelimiter(), values); + } + + return values.get(0); + } + + private static List collectUserVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) { + Room room = ctx.room(); + List users = WiredSourceUtil.resolveUsers(ctx, extra.getUserSource()); + if (room == null || users.isEmpty()) { + return List.of(); + } + + LinkedHashSet seenUserIds = new LinkedHashSet<>(); + List values = new ArrayList<>(); + + for (RoomUnit roomUnit : users) { + if (roomUnit == null || !seenUserIds.add(roomUnit.getId())) { + continue; + } + + String value = resolveUserVariableValue(room, roomUnit, extra); + if (value != null && !value.isEmpty()) { + values.add(value); + } + } + + return values; + } + + private static List collectFurniVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) { + Room room = ctx.room(); + if (room == null) { + return List.of(); + } + + extra.refresh(room); + + List items = (extra.getFurniSource() == WiredSourceUtil.SOURCE_SELECTOR) + ? WiredSourceUtil.resolveSelectorItems(ctx, true) + : WiredSourceUtil.resolveItems(ctx, extra.getFurniSource(), extra.getItems()); + + if (items.isEmpty()) { + return List.of(); + } + + LinkedHashSet seenItemIds = new LinkedHashSet<>(); + List values = new ArrayList<>(); + + for (HabboItem item : items) { + if (item == null || !seenItemIds.add(item.getId())) { + continue; + } + + String value = resolveFurniVariableValue(room, item, extra); + if (value != null && !value.isEmpty()) { + values.add(value); + } + } + + return values; + } + + private static List collectRoomVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) { + Room room = ctx.room(); + if (room == null) { + return List.of(); + } + + String value = resolveRoomVariableValue(room, extra); + return (value == null || value.isEmpty()) ? List.of() : List.of(value); + } + + private static List collectContextVariableValues(WiredContext ctx, WiredExtraTextOutputVariable extra) { + if (ctx == null) { + return List.of(); + } + + String value = resolveContextVariableValue(ctx, extra); + return (value == null || value.isEmpty()) ? List.of() : List.of(value); + } + + private static String resolveUserVariableValue(Room room, RoomUnit roomUnit, WiredExtraTextOutputVariable extra) { + if (room == null || roomUnit == null) { + return null; + } + + if (WiredExtraTextOutputVariable.isInternalVariableToken(extra.getVariableToken())) { + Integer value = readUserInternalValue(room, roomUnit, WiredExtraTextOutputVariable.getInternalVariableKey(extra.getVariableToken())); + return value != null ? String.valueOf(value) : null; + } + + Habbo habbo = room.getHabbo(roomUnit); + if (habbo == null || !room.getUserVariableManager().hasVariable(habbo.getHabboInfo().getId(), extra.getVariableItemId())) { + return null; + } + + Integer value = room.getUserVariableManager().getCurrentValue(habbo.getHabboInfo().getId(), extra.getVariableItemId()); + if (extra.getDisplayType(room) == WiredExtraTextOutputVariable.DISPLAY_TEXTUAL) { + return WiredVariableTextConnectorSupport.toText(room, extra.getVariableItemId(), value); + } + + return value != null ? String.valueOf(value) : null; + } + + private static String resolveFurniVariableValue(Room room, HabboItem item, WiredExtraTextOutputVariable extra) { + if (room == null || item == null) { + return null; + } + + if (WiredExtraTextOutputVariable.isInternalVariableToken(extra.getVariableToken())) { + Integer value = readFurniInternalValue(room, item, WiredExtraTextOutputVariable.getInternalVariableKey(extra.getVariableToken())); + return value != null ? String.valueOf(value) : null; + } + + if (!room.getFurniVariableManager().hasVariable(item.getId(), extra.getVariableItemId())) { + return null; + } + + Integer value = room.getFurniVariableManager().getCurrentValue(item.getId(), extra.getVariableItemId()); + if (extra.getDisplayType(room) == WiredExtraTextOutputVariable.DISPLAY_TEXTUAL) { + return WiredVariableTextConnectorSupport.toText(room, extra.getVariableItemId(), value); + } + + return value != null ? String.valueOf(value) : null; + } + + private static String resolveRoomVariableValue(Room room, WiredExtraTextOutputVariable extra) { + if (room == null) { + return null; + } + + if (WiredExtraTextOutputVariable.isInternalVariableToken(extra.getVariableToken())) { + Integer value = readRoomInternalValue(room, WiredExtraTextOutputVariable.getInternalVariableKey(extra.getVariableToken())); + return value != null ? String.valueOf(value) : null; + } + + Integer value = room.getRoomVariableManager().getCurrentValue(extra.getVariableItemId()); + if (extra.getDisplayType(room) == WiredExtraTextOutputVariable.DISPLAY_TEXTUAL) { + return WiredVariableTextConnectorSupport.toText(room, extra.getVariableItemId(), value); + } + + return value != null ? String.valueOf(value) : null; + } + + private static String resolveContextVariableValue(WiredContext ctx, WiredExtraTextOutputVariable extra) { + if (ctx == null) { + return null; + } + + if (WiredExtraTextOutputVariable.isInternalVariableToken(extra.getVariableToken())) { + Integer value = WiredInternalVariableSupport.readContextValue(ctx, WiredExtraTextOutputVariable.getInternalVariableKey(extra.getVariableToken())); + return value != null ? String.valueOf(value) : null; + } + + if (!WiredContextVariableSupport.hasVariable(ctx, extra.getVariableItemId())) { + return null; + } + + Integer value = WiredContextVariableSupport.getCurrentValue(ctx, extra.getVariableItemId()); + if (extra.getDisplayType(ctx.room()) == WiredExtraTextOutputVariable.DISPLAY_TEXTUAL) { + return WiredVariableTextConnectorSupport.toText(ctx.room(), extra.getVariableItemId(), value); + } + + return value != null ? String.valueOf(value) : null; + } + + private static String getRoomUnitName(Room room, RoomUnit roomUnit) { + if (room == null || roomUnit == null) { + return ""; + } + + if (roomUnit.getRoomUnitType() == RoomUnitType.USER) { + Habbo habbo = room.getHabbo(roomUnit); + return (habbo != null && habbo.getHabboInfo() != null) ? habbo.getHabboInfo().getUsername() : ""; + } + + if (roomUnit.getRoomUnitType() == RoomUnitType.BOT) { + Bot bot = room.getBot(roomUnit); + return (bot != null) ? bot.getName() : ""; + } + + if (roomUnit.getRoomUnitType() == RoomUnitType.PET) { + Pet pet = room.getPet(roomUnit); + return (pet != null) ? pet.getName() : ""; + } + + return ""; + } + + private static Integer readUserInternalValue(Room room, RoomUnit roomUnit, String key) { + return WiredInternalVariableSupport.readUserValue(room, roomUnit, key); + } + + private static Integer readFurniInternalValue(Room room, HabboItem item, String key) { + return WiredInternalVariableSupport.readFurniValue(room, item, key); + } + + private static Integer readRoomInternalValue(Room room, String key) { + return WiredInternalVariableSupport.readRoomValue(room, key); + } + + private static Integer getUserTeamScore(Room room, Habbo habbo) { + if (room == null || habbo == null || habbo.getHabboInfo().getGamePlayer() == null) { + return null; + } + + Game game = resolveTeamGame(room, habbo); + GamePlayer gamePlayer = habbo.getHabboInfo().getGamePlayer(); + + if (game == null || gamePlayer.getTeamColor() == null) { + return gamePlayer.getScore(); + } + + GameTeam team = game.getTeam(gamePlayer.getTeamColor()); + return (team != null) ? team.getTotalScore() : gamePlayer.getScore(); + } + + private static Integer getTeamColorId(int effectId) { + TeamEffectData data = getTeamEffectData(effectId); + return data == null ? null : data.colorId; + } + + private static Integer getTeamTypeId(int effectId) { + TeamEffectData data = getTeamEffectData(effectId); + return data == null ? null : data.typeId; + } + + private static int getTeamMetric(Room room, GameTeamColors color, boolean score) { + Game game = resolveTeamGame(room, null); + if (game == null || color == null) { + return 0; + } + + GameTeam team = game.getTeam(color); + if (team == null) { + return 0; + } + + return score ? team.getTotalScore() : team.getMembers().size(); + } + + private static Game resolveTeamGame(Room room, Habbo habbo) { + if (room == null) { + return null; + } + + if (habbo != null && habbo.getHabboInfo() != null && habbo.getHabboInfo().getCurrentGame() != null) { + Game game = room.getGame(habbo.getHabboInfo().getCurrentGame()); + if (game != null) { + return game; + } + } + + Game wiredGame = room.getGame(WiredGame.class); + if (wiredGame != null) { + return wiredGame; + } + + Game freezeGame = room.getGame(FreezeGame.class); + if (freezeGame != null) { + return freezeGame; + } + + return room.getGame(BattleBanzaiGame.class); + } + + private static TeamEffectData getTeamEffectData(int effectValue) { + if (effectValue <= 0) { + return null; + } + + if (effectValue >= 223 && effectValue <= 226) { + return new TeamEffectData(effectValue - 222, 0); + } + + if (effectValue >= 33 && effectValue <= 36) { + return new TeamEffectData(effectValue - 32, 1); + } + + if (effectValue >= 40 && effectValue <= 43) { + return new TeamEffectData(effectValue - 39, 2); + } + + return null; + } + + private static Integer parseInteger(String value) { + if (value == null || value.trim().isEmpty()) { + return null; + } + + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException ignored) { + return null; + } + } + + private static class TeamEffectData { + final int colorId; + final int typeId; + + TeamEffectData(int colorId, int typeId) { + this.colorId = colorId; + this.typeId = typeId; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTriggerSourceUtil.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTriggerSourceUtil.java new file mode 100644 index 00000000..15c50f03 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredTriggerSourceUtil.java @@ -0,0 +1,167 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurni; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterFurniByVariable; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUser; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraFilterUsersByVariable; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.HabboItem; +import gnu.trove.set.hash.THashSet; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public final class WiredTriggerSourceUtil { + private WiredTriggerSourceUtil() { + } + + public static List resolveItems(InteractionWiredTrigger trigger, + WiredEvent event, + int sourceType, + Collection selectedItems) { + switch (sourceType) { + case WiredSourceUtil.SOURCE_TRIGGER: + return event.getSourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); + case WiredSourceUtil.SOURCE_SELECTED: + return (selectedItems != null) ? new ArrayList<>(selectedItems) : Collections.emptyList(); + case WiredSourceUtil.SOURCE_SELECTOR: + return resolveSelectorItems(trigger, event); + case WiredSourceUtil.SOURCE_SIGNAL: + if (event.getType() == WiredEvent.Type.SIGNAL_RECEIVED) { + return event.getSourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); + } + return Collections.emptyList(); + default: + return event.getSourceItem().map(Collections::singletonList).orElse(Collections.emptyList()); + } + } + + public static List resolveUsers(InteractionWiredTrigger trigger, + WiredEvent event, + int sourceType, + Collection selectedUsers) { + switch (sourceType) { + case WiredSourceUtil.SOURCE_TRIGGER: + return event.getActor().map(Collections::singletonList).orElse(Collections.emptyList()); + case WiredSourceUtil.SOURCE_SELECTED: + return (selectedUsers != null) ? new ArrayList<>(selectedUsers) : Collections.emptyList(); + case WiredSourceUtil.SOURCE_SELECTOR: + return resolveSelectorUsers(trigger, event); + case WiredSourceUtil.SOURCE_SIGNAL: + if (event.getType() == WiredEvent.Type.SIGNAL_RECEIVED) { + return event.getActor().map(Collections::singletonList).orElse(Collections.emptyList()); + } + return Collections.emptyList(); + default: + return event.getActor().map(Collections::singletonList).orElse(Collections.emptyList()); + } + } + + public static boolean containsItemOrTile(Room room, Collection items, HabboItem sourceItem) { + if (room == null || items == null || items.isEmpty() || sourceItem == null) { + return false; + } + + if (items.contains(sourceItem)) { + return true; + } + + for (HabboItem item : room.getItemsAt(sourceItem.getX(), sourceItem.getY())) { + if (items.contains(item)) { + return true; + } + } + + return false; + } + + public static boolean containsUser(Collection users, RoomUnit target) { + if (users == null || users.isEmpty() || target == null) { + return false; + } + + for (RoomUnit user : users) { + if (user != null && user.getId() == target.getId()) { + return true; + } + } + + return false; + } + + private static List resolveSelectorItems(InteractionWiredTrigger trigger, WiredEvent event) { + WiredContext ctx = executeSelectors(trigger, event); + + if (ctx == null || !ctx.targets().isItemsModifiedBySelector()) { + return Collections.emptyList(); + } + + return new ArrayList<>(ctx.targets().items()); + } + + private static List resolveSelectorUsers(InteractionWiredTrigger trigger, WiredEvent event) { + WiredContext ctx = executeSelectors(trigger, event); + + if (ctx == null || !ctx.targets().isUsersModifiedBySelector()) { + return Collections.emptyList(); + } + + return new ArrayList<>(ctx.targets().users()); + } + + private static WiredContext executeSelectors(InteractionWiredTrigger trigger, WiredEvent event) { + if (trigger == null || event == null) { + return null; + } + + Room room = event.getRoom(); + if (room == null || room.getRoomSpecialTypes() == null) { + return null; + } + + WiredContext ctx = new WiredContext(event, trigger, DefaultWiredServices.getInstance(), new WiredState(100)); + + for (InteractionWiredEffect effect : getOrderedSelectorEffects(room, trigger)) { + if (effect.requiresActor() && !ctx.hasActor()) { + continue; + } + + try { + ctx.state().step(); + effect.execute(ctx); + } catch (Exception ignored) { + } + } + + applySelectionFilterExtras(room, trigger, ctx); + + return ctx; + } + + private static List getOrderedSelectorEffects(Room room, InteractionWiredTrigger trigger) { + if (room == null || trigger == null || room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + THashSet effects = room.getRoomSpecialTypes().getEffects(trigger.getX(), trigger.getY()); + List selectorEffects = new ArrayList<>(); + + for (InteractionWiredEffect effect : WiredExecutionOrderUtil.sort(effects)) { + if (effect != null && effect.isSelector()) { + selectorEffects.add(effect); + } + } + + return selectorEffects; + } + + private static void applySelectionFilterExtras(Room room, HabboItem triggerItem, WiredContext selectorCtx) { + WiredSelectionFilterSupport.applySelectorFilters(room, triggerItem, selectorCtx); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredUserMovementHelper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredUserMovementHelper.java new file mode 100644 index 00000000..a08a64ba --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredUserMovementHelper.java @@ -0,0 +1,590 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.InteractionStackWalkHelper; +import com.eu.habbo.habbohotel.items.interactions.InteractionTileWalkMagic; +import com.eu.habbo.habbohotel.items.interactions.interfaces.ConditionalGate; +import com.eu.habbo.habbohotel.items.interactions.InteractionRoller; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomLayout; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.rooms.RoomUserRotation; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.outgoing.rooms.WiredMovementsComposer; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer; +import gnu.trove.set.hash.THashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class WiredUserMovementHelper { + public static final int DEFAULT_ANIMATION_DURATION = WiredMovementsComposer.DEFAULT_DURATION; + private static final int SUPPRESS_NEXT_WALK_WINDOW_MS = 250; + private static final int STATUS_SUPPRESSION_GRACE_MS = 250; + private static final String SUPPRESS_NEXT_WALK_CACHE_KEY = "wired_suppress_next_walk_until"; + + private static final Logger LOGGER = LoggerFactory.getLogger(WiredUserMovementHelper.class); + private static final ThreadLocal> SUPPRESSED_STATUS_ROOM_UNIT_IDS = new ThreadLocal<>(); + private static final ConcurrentHashMap SUPPRESSED_STATUS_COMPOSER_UNTIL = new ConcurrentHashMap<>(); + + private WiredUserMovementHelper() { + } + + public static boolean moveUser(Room room, RoomUnit roomUnit, RoomTile targetTile, double targetZ, int duration) { + return moveUser(room, roomUnit, targetTile, targetZ, roomUnit == null ? null : roomUnit.getBodyRotation(), + roomUnit == null ? null : roomUnit.getHeadRotation(), duration, false, WiredMovementPhysics.NONE); + } + + public static boolean moveUser(Room room, RoomUnit roomUnit, RoomTile targetTile, double targetZ, RoomUserRotation bodyRotation, RoomUserRotation headRotation, int duration) { + return moveUser(room, roomUnit, targetTile, targetZ, bodyRotation, headRotation, duration, false, WiredMovementPhysics.NONE); + } + + public static boolean moveUser(Room room, RoomUnit roomUnit, RoomTile targetTile, double targetZ, RoomUserRotation bodyRotation, RoomUserRotation headRotation, int duration, boolean noAnimation) { + return moveUser(room, roomUnit, targetTile, targetZ, bodyRotation, headRotation, duration, noAnimation, WiredMovementPhysics.NONE); + } + + public static boolean moveUser(Room room, RoomUnit roomUnit, RoomTile targetTile, double targetZ, RoomUserRotation bodyRotation, RoomUserRotation headRotation, int duration, boolean noAnimation, WiredMovementPhysics movementPhysics) { + if (room == null || roomUnit == null || targetTile == null || room.getLayout() == null) { + return false; + } + + RoomTile oldLocation = roomUnit.getCurrentLocation(); + WiredMovementPhysics resolvedMovementPhysics = movementPhysics == null ? WiredMovementPhysics.NONE : movementPhysics; + + if (oldLocation == null || !canMoveTo(room, roomUnit, targetTile, resolvedMovementPhysics)) { + return false; + } + + RoomUserRotation resolvedBodyRotation = bodyRotation == null ? roomUnit.getBodyRotation() : bodyRotation; + RoomUserRotation resolvedHeadRotation = headRotation == null ? roomUnit.getHeadRotation() : headRotation; + + if (noAnimation) { + return moveUserInstant( + room, + roomUnit, + oldLocation, + targetTile, + roomUnit.getZ(), + targetZ, + resolvedBodyRotation, + resolvedHeadRotation, + room.getTopItemAt(oldLocation.x, oldLocation.y), + resolveEnteredItem(room, targetTile), + room.getHabbo(roomUnit)); + } + + double oldZ = roomUnit.getZ(); + HabboItem oldTopItem = room.getTopItemAt(oldLocation.x, oldLocation.y); + HabboItem newTopItem = resolveEnteredItem(room, targetTile); + Habbo habbo = room.getHabbo(roomUnit); + + int animationDuration = (duration > 0) ? duration : DEFAULT_ANIMATION_DURATION; + + runWithSuppressedStatusUpdates(Collections.singletonList(roomUnit), () -> { + roomUnit.removeStatus(RoomUnitStatus.MOVE); + roomUnit.setZ(targetZ); + roomUnit.setLocation(targetTile); + roomUnit.setPath(new LinkedList<>()); + roomUnit.setBodyRotation(resolvedBodyRotation); + roomUnit.setHeadRotation(resolvedHeadRotation); + roomUnit.resetIdleTimer(); + + if (habbo != null) { + THashSet movedHabbos = new THashSet<>(); + movedHabbos.add(habbo); + room.updateHabbosAt(targetTile.x, targetTile.y, movedHabbos); + } + + roomUnit.statusUpdate(false); + }); + + List movements = new ArrayList<>(); + movements.add(WiredMovementsComposer.userSlideMovement( + roomUnit.getId(), + oldLocation.x, + oldLocation.y, + targetTile.x, + targetTile.y, + oldZ, + roomUnit.getZ(), + resolvedBodyRotation.getValue(), + resolvedHeadRotation.getValue(), + animationDuration)); + suppressStatusComposer(roomUnit, animationDuration); + room.sendComposer(new WiredMovementsComposer(movements).compose()); + + scheduleTileCallbacks(room, roomUnit, oldLocation, targetTile, oldTopItem, newTopItem, animationDuration); + scheduleFinalStatusSync(room, roomUnit, targetTile, animationDuration); + schedulePostureSync(room, roomUnit, targetTile, animationDuration); + return true; + } + + public static void suppressNextWalkCommand(RoomUnit roomUnit) { + if (roomUnit == null || roomUnit.getCacheable() == null) { + return; + } + + roomUnit.getCacheable().put(SUPPRESS_NEXT_WALK_CACHE_KEY, System.currentTimeMillis() + SUPPRESS_NEXT_WALK_WINDOW_MS); + } + + public static boolean consumeSuppressedWalkCommand(RoomUnit roomUnit) { + if (roomUnit == null || roomUnit.getCacheable() == null) { + return false; + } + + Object value = roomUnit.getCacheable().remove(SUPPRESS_NEXT_WALK_CACHE_KEY); + + if (!(value instanceof Long)) { + return false; + } + + return ((Long) value) >= System.currentTimeMillis(); + } + + private static boolean moveUserInstant(Room room, RoomUnit roomUnit, RoomTile oldLocation, RoomTile targetTile, double oldZ, double targetZ, + RoomUserRotation bodyRotation, RoomUserRotation headRotation, HabboItem oldTopItem, + HabboItem newTopItem, Habbo habbo) { + suppressNextWalkCommand(roomUnit); + roomUnit.removeStatus(RoomUnitStatus.MOVE); + roomUnit.setPath(new LinkedList<>()); + roomUnit.setBodyRotation(bodyRotation); + roomUnit.setHeadRotation(headRotation); + roomUnit.setCurrentLocation(targetTile); + roomUnit.setGoalLocation(targetTile); + roomUnit.setZ(targetZ); + roomUnit.setPreviousLocation(oldLocation); + roomUnit.setPreviousLocationZ(oldZ); + roomUnit.resetIdleTimer(); + roomUnit.statusUpdate(true); + + if (habbo != null) { + THashSet movedHabbos = new THashSet<>(); + movedHabbos.add(habbo); + room.updateHabbosAt(targetTile.x, targetTile.y, movedHabbos); + } else { + switch (roomUnit.getRoomUnitType()) { + case BOT -> room.updateBotsAt(targetTile.x, targetTile.y); + case PET -> room.updatePetsAt(targetTile.x, targetTile.y); + } + } + + processTileCallbacks(room, roomUnit, oldLocation, targetTile, oldTopItem, newTopItem); + clearStatusComposerSuppression(roomUnit); + room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); + return true; + } + + public static boolean updateUserDirection(Room room, RoomUnit roomUnit, RoomUserRotation bodyRotation, RoomUserRotation headRotation) { + if (room == null || roomUnit == null) { + return false; + } + + RoomUserRotation resolvedBodyRotation = bodyRotation == null ? roomUnit.getBodyRotation() : bodyRotation; + RoomUserRotation resolvedHeadRotation = headRotation == null ? roomUnit.getHeadRotation() : headRotation; + + roomUnit.setBodyRotation(resolvedBodyRotation); + roomUnit.setHeadRotation(resolvedHeadRotation); + room.sendComposer(new WiredMovementsComposer(Collections.singletonList( + WiredMovementsComposer.userDirectionUpdate( + roomUnit.getId(), + resolvedHeadRotation.getValue(), + resolvedBodyRotation.getValue()))).compose()); + return true; + } + + public static boolean shouldSuppressStatusUpdate(RoomUnit roomUnit) { + if (roomUnit == null) { + return false; + } + + Set suppressedRoomUnitIds = SUPPRESSED_STATUS_ROOM_UNIT_IDS.get(); + return suppressedRoomUnitIds != null && suppressedRoomUnitIds.contains(roomUnit.getId()); + } + + public static boolean shouldSuppressStatusComposer(RoomUnit roomUnit) { + if (roomUnit == null) { + return false; + } + + Long suppressedUntil = SUPPRESSED_STATUS_COMPOSER_UNTIL.get(roomUnit.getId()); + + if (suppressedUntil == null) { + return false; + } + + if (suppressedUntil <= System.currentTimeMillis()) { + SUPPRESSED_STATUS_COMPOSER_UNTIL.remove(roomUnit.getId(), suppressedUntil); + return false; + } + + return true; + } + + public static void clearStatusComposerSuppression(RoomUnit roomUnit) { + if (roomUnit == null) { + return; + } + + SUPPRESSED_STATUS_COMPOSER_UNTIL.remove(roomUnit.getId()); + } + + public static boolean canMoveTo(Room room, RoomUnit roomUnit, RoomTile targetTile, WiredMovementPhysics movementPhysics) { + if (room == null || roomUnit == null || targetTile == null) { + return false; + } + + WiredMovementPhysics resolvedMovementPhysics = movementPhysics == null ? WiredMovementPhysics.NONE : movementPhysics; + + if (targetTile.state == null || targetTile.state == com.eu.habbo.habbohotel.rooms.RoomTileState.INVALID) { + return false; + } + + if (targetTile.state == com.eu.habbo.habbohotel.rooms.RoomTileState.BLOCKED + && !canBypassBlockedTile(room, targetTile, resolvedMovementPhysics)) { + return false; + } + + if (!room.getLayout().tileWalkable(targetTile.x, targetTile.y) + && !room.canSitOrLayAt(targetTile.x, targetTile.y) + && !canBypassBlockedTile(room, targetTile, resolvedMovementPhysics)) { + return false; + } + + return !hasBlockingUnits(room, roomUnit, targetTile, resolvedMovementPhysics); + } + + private static boolean hasBlockingUnits(Room room, RoomUnit roomUnit, RoomTile targetTile, WiredMovementPhysics movementPhysics) { + Collection units = room.getRoomUnitsAt(targetTile); + + if (units == null || units.isEmpty()) { + return false; + } + + for (RoomUnit targetUnit : units) { + if (targetUnit != null + && targetUnit != roomUnit + && !movementPhysics.shouldIgnoreUser(targetUnit)) { + return true; + } + } + + return false; + } + + private static boolean canBypassBlockedTile(Room room, RoomTile targetTile, WiredMovementPhysics movementPhysics) { + if (room == null || targetTile == null || movementPhysics == null || !movementPhysics.isActive()) { + return false; + } + + Collection items = room.getItemsAt(targetTile); + if (items == null || items.isEmpty()) { + return false; + } + + boolean hasIgnoredFurni = false; + + for (HabboItem item : items) { + if (item == null) { + continue; + } + + if (movementPhysics.isBlockingFurni(item)) { + return false; + } + + if (movementPhysics.shouldIgnoreFurni(item)) { + hasIgnoredFurni = true; + continue; + } + + if (!item.isWalkable() + && !item.getBaseItem().allowSit() + && !item.getBaseItem().allowLay()) { + return false; + } + } + + return hasIgnoredFurni; + } + + private static void scheduleTileCallbacks(Room room, RoomUnit roomUnit, RoomTile oldLocation, RoomTile targetTile, HabboItem oldTopItem, HabboItem newTopItem, int delay) { + Emulator.getThreading().run(() -> { + processTileCallbacks(room, roomUnit, oldLocation, targetTile, oldTopItem, newTopItem); + }, Math.max(delay, 1)); + } + + private static void processTileCallbacks(Room room, RoomUnit roomUnit, RoomTile oldLocation, RoomTile targetTile, HabboItem oldTopItem, HabboItem newTopItem) { + if (room == null || !room.isLoaded() || roomUnit == null || roomUnit.getCurrentLocation() == null) { + return; + } + + if (roomUnit.getCurrentLocation().x != targetTile.x || roomUnit.getCurrentLocation().y != targetTile.y) { + return; + } + + HabboItem resolvedNewTopItem = resolveEnteredItem(room, targetTile); + if (resolvedNewTopItem == null) { + resolvedNewTopItem = room.getTopItemAt(targetTile.x, targetTile.y); + } + if (resolvedNewTopItem == null) { + resolvedNewTopItem = newTopItem; + } + + if (oldTopItem != null && (oldTopItem != resolvedNewTopItem || !occupiesTile(oldTopItem, targetTile))) { + try { + oldTopItem.onWalkOff(roomUnit, room, new Object[]{oldLocation, targetTile}); + } catch (Exception exception) { + LOGGER.error("Failed to process wired user walk off callback", exception); + } + } + + for (HabboItem additionalOldItem : resolveAdditionalTileItems(room, oldLocation, oldTopItem)) { + if (additionalOldItem == resolvedNewTopItem && occupiesTile(additionalOldItem, targetTile)) { + continue; + } + + try { + additionalOldItem.onWalkOff(roomUnit, room, new Object[]{oldLocation, targetTile}); + } catch (Exception exception) { + LOGGER.error("Failed to process additional wired user walk off callback", exception); + } + } + + if (resolvedNewTopItem != null) { + try { + if (resolvedNewTopItem != oldTopItem || !occupiesTile(resolvedNewTopItem, oldLocation)) { + if (!resolvedNewTopItem.canWalkOn(roomUnit, room, null)) { + if (resolvedNewTopItem instanceof ConditionalGate) { + roomUnit.setLocation(oldLocation); + roomUnit.setZ(oldLocation.getStackHeight()); + roomUnit.setPreviousLocation(oldLocation); + roomUnit.setPreviousLocationZ(oldLocation.getStackHeight()); + room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); + return; + } + } else { + resolvedNewTopItem.onWalkOn(roomUnit, room, new Object[]{oldLocation, targetTile}); + } + } else { + resolvedNewTopItem.onWalk(roomUnit, room, new Object[]{oldLocation, targetTile}); + } + } catch (Exception exception) { + LOGGER.error("Failed to process wired user walk on callback", exception); + } + } + + for (HabboItem additionalNewItem : resolveAdditionalTileItems(room, targetTile, resolvedNewTopItem)) { + try { + if (occupiesTile(additionalNewItem, oldLocation)) { + additionalNewItem.onWalk(roomUnit, room, new Object[]{oldLocation, targetTile}); + } else { + additionalNewItem.onWalkOn(roomUnit, room, new Object[]{oldLocation, targetTile}); + } + } catch (Exception exception) { + LOGGER.error("Failed to process additional wired user walk on callback", exception); + } + } + } + + private static HabboItem resolveEnteredItem(Room room, RoomTile tile) { + if (room == null || tile == null || room.getItemManager() == null) { + return null; + } + + if (room.canSitAt(tile.x, tile.y)) { + HabboItem tallestChair = room.getTallestChair(tile); + if (tallestChair != null) { + return tallestChair; + } + } + + HabboItem candidate = null; + + for (HabboItem item : room.getItemsAt(tile)) { + if (item == null || !occupiesTile(item, tile)) { + continue; + } + + boolean preferred = item instanceof ConditionalGate + || item.isWalkable() + || item.getBaseItem().allowWalk() + || item.getBaseItem().allowSit() + || item.getBaseItem().allowLay(); + + if (!preferred) { + continue; + } + + if (candidate == null || item.getZ() >= candidate.getZ()) { + candidate = item; + } + } + + if (candidate != null) { + return candidate; + } + + return room.getTopItemAt(tile.x, tile.y); + } + + public static double resolveUserTargetZ(Room room, RoomTile targetTile) { + if (room == null || targetTile == null || room.getLayout() == null) { + return 0; + } + + HabboItem targetItem = resolveEnteredItem(room, targetTile); + double targetZ = room.getLayout().getHeightAtSquare(targetTile.x, targetTile.y); + + if (targetItem != null) { + targetZ = targetItem.getZ(); + + if (!targetItem.getBaseItem().allowSit() && !targetItem.getBaseItem().allowLay()) { + targetZ += Item.getCurrentHeight(targetItem); + } + } + + for (HabboItem item : room.getItemsAt(targetTile)) { + if (item instanceof InteractionTileWalkMagic || item instanceof InteractionStackWalkHelper) { + targetZ = item.getZ(); + break; + } + } + + return targetZ; + } + + private static boolean occupiesTile(HabboItem item, RoomTile tile) { + if (item == null || tile == null || item.getBaseItem() == null) { + return false; + } + + return RoomLayout.pointInSquare( + item.getX(), + item.getY(), + item.getX() + item.getBaseItem().getWidth() - 1, + item.getY() + item.getBaseItem().getLength() - 1, + tile.x, + tile.y); + } + + private static List resolveAdditionalTileItems(Room room, RoomTile tile, HabboItem primaryItem) { + if (room == null || tile == null) { + return Collections.emptyList(); + } + + List items = new ArrayList<>(); + + for (HabboItem item : room.getItemsAt(tile)) { + if (item == null || item == primaryItem || !occupiesTile(item, tile)) { + continue; + } + + items.add(item); + } + + items.sort(Comparator + .comparingDouble((HabboItem item) -> item.getZ() + Item.getCurrentHeight(item)) + .thenComparingInt(HabboItem::getId) + .reversed()); + return items; + } + + private static void schedulePostureSync(Room room, RoomUnit roomUnit, RoomTile targetTile, int delay) { + if (!roomUnit.hasStatus(RoomUnitStatus.SIT) && !roomUnit.hasStatus(RoomUnitStatus.LAY)) { + return; + } + + Emulator.getThreading().run(() -> { + if (room == null || !room.isLoaded() || roomUnit == null || roomUnit.getCurrentLocation() == null) { + return; + } + + if (roomUnit.getCurrentLocation().x != targetTile.x || roomUnit.getCurrentLocation().y != targetTile.y) { + return; + } + + room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); + }, delay + STATUS_SUPPRESSION_GRACE_MS + 25); + } + + private static void scheduleFinalStatusSync(Room room, RoomUnit roomUnit, RoomTile targetTile, int delay) { + if (room == null || roomUnit == null || targetTile == null) { + return; + } + + Emulator.getThreading().run(() -> { + if (room == null || !room.isLoaded() || roomUnit == null || roomUnit.getCurrentLocation() == null) { + return; + } + + if (roomUnit.isWalking() + || roomUnit.getCurrentLocation().x != targetTile.x + || roomUnit.getCurrentLocation().y != targetTile.y) { + return; + } + + clearStatusComposerSuppression(roomUnit); + roomUnit.setPreviousLocation(roomUnit.getCurrentLocation()); + roomUnit.setPreviousLocationZ(roomUnit.getZ()); + room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); + }, Math.max(delay, 1) + 25); + } + + private static void suppressStatusComposer(RoomUnit roomUnit, int duration) { + if (roomUnit == null) { + return; + } + + long suppressedUntil = System.currentTimeMillis() + Math.max(duration, InteractionRoller.DELAY) + STATUS_SUPPRESSION_GRACE_MS; + SUPPRESSED_STATUS_COMPOSER_UNTIL.put(roomUnit.getId(), suppressedUntil); + } + + private static void runWithSuppressedStatusUpdates(Collection roomUnits, Runnable action) { + if (action == null) { + return; + } + + Set previousSuppressedRoomUnitIds = SUPPRESSED_STATUS_ROOM_UNIT_IDS.get(); + HashSet suppressedRoomUnitIds = previousSuppressedRoomUnitIds == null + ? new HashSet<>() + : new HashSet<>(previousSuppressedRoomUnitIds); + + if (roomUnits != null) { + for (RoomUnit roomUnit : roomUnits) { + if (roomUnit != null) { + suppressedRoomUnitIds.add(roomUnit.getId()); + } + } + } + + if (suppressedRoomUnitIds.isEmpty()) { + action.run(); + return; + } + + SUPPRESSED_STATUS_ROOM_UNIT_IDS.set(suppressedRoomUnitIds); + + try { + action.run(); + } finally { + if (previousSuppressedRoomUnitIds == null || previousSuppressedRoomUnitIds.isEmpty()) { + SUPPRESSED_STATUS_ROOM_UNIT_IDS.remove(); + } else { + SUPPRESSED_STATUS_ROOM_UNIT_IDS.set(previousSuppressedRoomUnitIds); + } + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableLevelSystemSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableLevelSystemSupport.java new file mode 100644 index 00000000..61de371f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableLevelSystemSupport.java @@ -0,0 +1,564 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +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.WiredExtraVariableLevelUpSystem; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableReference; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import gnu.trove.set.hash.THashSet; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class WiredVariableLevelSystemSupport { + public static final int TARGET_USER = 0; + public static final int TARGET_FURNI = 1; + public static final int TARGET_ROOM = 3; + + private static final int SYNTHETIC_USER_OFFSET = 700_000_000; + private static final int SYNTHETIC_FURNI_OFFSET = 800_000_000; + private static final int SYNTHETIC_ROOM_OFFSET = 900_000_000; + private static final int SYNTHETIC_STRIDE = 16; + + private WiredVariableLevelSystemSupport() { + } + + public static WiredExtraVariableLevelUpSystem getLevelSystem(Room room, InteractionWiredExtra definition) { + if (room == null || definition == null || room.getRoomSpecialTypes() == null) { + return null; + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(definition.getX(), definition.getY()); + if (extras == null || extras.isEmpty()) { + return null; + } + + for (InteractionWiredExtra extra : WiredExecutionOrderUtil.sort(extras)) { + if (extra instanceof WiredExtraVariableLevelUpSystem) { + return (WiredExtraVariableLevelUpSystem) extra; + } + } + + return null; + } + + public static List getDerivedDefinitions(Room room, int targetType, InteractionWiredExtra definitionExtra, WiredVariableDefinitionInfo baseDefinition) { + if (room == null || definitionExtra == null || baseDefinition == null || !baseDefinition.hasValue()) { + return Collections.emptyList(); + } + + WiredExtraVariableLevelUpSystem levelSystem = getLevelSystem(room, definitionExtra); + if (levelSystem == null) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + + for (int subvariableType : levelSystem.getSelectedSubvariables()) { + result.add(new WiredVariableDefinitionInfo( + createSyntheticItemId(targetType, baseDefinition.getItemId(), subvariableType), + baseDefinition.getName() + "." + getSubvariableKey(subvariableType), + true, + baseDefinition.getAvailability(), + false, + true + )); + } + + result.sort(Comparator.comparing(WiredVariableDefinitionInfo::getName, String.CASE_INSENSITIVE_ORDER).thenComparingInt(WiredVariableDefinitionInfo::getItemId)); + return result; + } + + public static WiredVariableDefinitionInfo getDerivedDefinitionInfo(Room room, int targetType, int syntheticItemId) { + DerivedDefinition derived = resolveDerivedDefinition(room, targetType, syntheticItemId); + + if (derived == null) { + return null; + } + + return new WiredVariableDefinitionInfo( + derived.syntheticItemId, + derived.variableName, + true, + derived.baseDefinition.getAvailability(), + false, + true + ); + } + + public static DerivedDefinition resolveDerivedDefinition(Room room, int targetType, int syntheticItemId) { + DecodedSyntheticId decoded = decodeSyntheticId(syntheticItemId); + if (decoded == null || decoded.targetType != targetType || room == null || room.getRoomSpecialTypes() == null) { + return null; + } + + InteractionWiredExtra baseExtra = room.getRoomSpecialTypes().getExtra(decoded.baseDefinitionItemId); + if (!matchesTarget(baseExtra, targetType)) { + return null; + } + + WiredVariableDefinitionInfo baseDefinition = createBaseDefinitionInfo(room, baseExtra, targetType); + if (baseDefinition == null || !baseDefinition.hasValue()) { + return null; + } + + WiredExtraVariableLevelUpSystem levelSystem = getLevelSystem(room, baseExtra); + if (levelSystem == null || !levelSystem.hasSubvariable(decoded.subvariableType)) { + return null; + } + + return new DerivedDefinition( + syntheticItemId, + decoded.baseDefinitionItemId, + decoded.subvariableType, + baseDefinition.getName() + "." + getSubvariableKey(decoded.subvariableType), + baseDefinition, + levelSystem + ); + } + + public static Integer getDerivedValue(WiredExtraVariableLevelUpSystem levelSystem, int subvariableType, Integer baseValue) { + if (levelSystem == null || baseValue == null) { + return null; + } + + LevelProgress progress = calculateProgress(levelSystem, baseValue); + return switch (subvariableType) { + case WiredExtraVariableLevelUpSystem.SUB_CURRENT_LEVEL -> progress.currentLevel; + case WiredExtraVariableLevelUpSystem.SUB_CURRENT_XP -> progress.currentXp; + case WiredExtraVariableLevelUpSystem.SUB_LEVEL_PROGRESS -> progress.progressXp; + case WiredExtraVariableLevelUpSystem.SUB_LEVEL_PROGRESS_PERCENT -> progress.progressPercent; + case WiredExtraVariableLevelUpSystem.SUB_TOTAL_XP_REQUIRED -> progress.totalXpRequired; + case WiredExtraVariableLevelUpSystem.SUB_XP_REMAINING -> progress.xpRemaining; + case WiredExtraVariableLevelUpSystem.SUB_IS_AT_MAX -> progress.isAtMax ? 1 : 0; + case WiredExtraVariableLevelUpSystem.SUB_MAX_LEVEL -> progress.maxLevel; + default -> null; + }; + } + + public static List buildPreviewEntries(WiredExtraVariableLevelUpSystem levelSystem) { + if (levelSystem == null) { + return Collections.emptyList(); + } + + return buildThresholdEntries(levelSystem); + } + + private static boolean matchesTarget(InteractionWiredExtra extra, int targetType) { + if (extra == null) { + return false; + } + + return switch (targetType) { + case TARGET_FURNI -> extra instanceof WiredExtraFurniVariable; + case TARGET_ROOM -> (extra instanceof WiredExtraRoomVariable) + || (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isRoomReference()); + default -> (extra instanceof WiredExtraUserVariable) + || (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isUserReference()); + }; + } + + private static WiredVariableDefinitionInfo createBaseDefinitionInfo(Room room, InteractionWiredExtra extra, int targetType) { + if (room == null || extra == null) { + return null; + } + + if (targetType == TARGET_FURNI && extra instanceof WiredExtraFurniVariable) { + WiredExtraFurniVariable definition = (WiredExtraFurniVariable) extra; + return new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(room, definition), + false + ); + } + + if (targetType == TARGET_USER) { + if (extra instanceof WiredExtraUserVariable) { + WiredExtraUserVariable definition = (WiredExtraUserVariable) extra; + return new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(room, definition), + false + ); + } + + if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isUserReference()) { + WiredExtraVariableReference reference = (WiredExtraVariableReference) extra; + return new WiredVariableDefinitionInfo(reference.getId(), reference.getVariableName(), reference.hasValue(), reference.getAvailability(), false, reference.isReadOnly()); + } + } + + if (targetType == TARGET_ROOM) { + if (extra instanceof WiredExtraRoomVariable) { + WiredExtraRoomVariable definition = (WiredExtraRoomVariable) extra; + return new WiredVariableDefinitionInfo( + definition.getId(), + definition.getVariableName(), + definition.hasValue(), + definition.getAvailability(), + WiredVariableTextConnectorSupport.isTextConnected(room, definition), + false + ); + } + + if (extra instanceof WiredExtraVariableReference && ((WiredExtraVariableReference) extra).isRoomReference()) { + WiredExtraVariableReference reference = (WiredExtraVariableReference) extra; + return new WiredVariableDefinitionInfo(reference.getId(), reference.getVariableName(), reference.hasValue(), reference.getAvailability(), false, reference.isReadOnly()); + } + } + + return null; + } + + private static int createSyntheticItemId(int targetType, int baseDefinitionItemId, int subvariableType) { + int offset = switch (targetType) { + case TARGET_FURNI -> SYNTHETIC_FURNI_OFFSET; + case TARGET_ROOM -> SYNTHETIC_ROOM_OFFSET; + default -> SYNTHETIC_USER_OFFSET; + }; + + return offset + (baseDefinitionItemId * SYNTHETIC_STRIDE) + (subvariableType + 1); + } + + private static DecodedSyntheticId decodeSyntheticId(int syntheticItemId) { + if (syntheticItemId >= SYNTHETIC_ROOM_OFFSET) { + return decodeSyntheticId(syntheticItemId, TARGET_ROOM, SYNTHETIC_ROOM_OFFSET); + } + + if (syntheticItemId >= SYNTHETIC_FURNI_OFFSET) { + return decodeSyntheticId(syntheticItemId, TARGET_FURNI, SYNTHETIC_FURNI_OFFSET); + } + + if (syntheticItemId >= SYNTHETIC_USER_OFFSET) { + return decodeSyntheticId(syntheticItemId, TARGET_USER, SYNTHETIC_USER_OFFSET); + } + + return null; + } + + private static DecodedSyntheticId decodeSyntheticId(int syntheticItemId, int targetType, int offset) { + int localValue = syntheticItemId - offset; + if (localValue < 0) { + return null; + } + + int encodedSubvariable = localValue % SYNTHETIC_STRIDE; + int baseDefinitionItemId = localValue / SYNTHETIC_STRIDE; + int subvariableType = encodedSubvariable - 1; + + if (baseDefinitionItemId <= 0 || subvariableType < 0 || subvariableType >= WiredExtraVariableLevelUpSystem.SUBVARIABLE_COUNT) { + return null; + } + + return new DecodedSyntheticId(targetType, baseDefinitionItemId, subvariableType); + } + + private static String getSubvariableKey(int subvariableType) { + return switch (subvariableType) { + case WiredExtraVariableLevelUpSystem.SUB_CURRENT_LEVEL -> "current_level"; + case WiredExtraVariableLevelUpSystem.SUB_CURRENT_XP -> "current_xp"; + case WiredExtraVariableLevelUpSystem.SUB_LEVEL_PROGRESS -> "level_progress"; + case WiredExtraVariableLevelUpSystem.SUB_LEVEL_PROGRESS_PERCENT -> "level_progress_percent"; + case WiredExtraVariableLevelUpSystem.SUB_TOTAL_XP_REQUIRED -> "total_xp_required"; + case WiredExtraVariableLevelUpSystem.SUB_XP_REMAINING -> "xp_remaining"; + case WiredExtraVariableLevelUpSystem.SUB_IS_AT_MAX -> "is_at_max"; + case WiredExtraVariableLevelUpSystem.SUB_MAX_LEVEL -> "max_level"; + default -> "value"; + }; + } + + private static LevelProgress calculateProgress(WiredExtraVariableLevelUpSystem levelSystem, int rawBaseValue) { + int currentXp = Math.max(0, rawBaseValue); + List entries = buildThresholdEntries(levelSystem); + + if (entries.isEmpty()) { + entries = new ArrayList<>(); + entries.add(new LevelEntry(1, 0)); + } + + int maxLevel = entries.get(entries.size() - 1).level; + int currentLevel = 1; + int currentThreshold = 0; + int nextThreshold = 0; + + for (int index = 0; index < entries.size(); index++) { + LevelEntry entry = entries.get(index); + + if (currentXp >= entry.requiredXp) { + currentLevel = entry.level; + currentThreshold = entry.requiredXp; + nextThreshold = (index + 1 < entries.size()) ? entries.get(index + 1).requiredXp : entry.requiredXp; + continue; + } + + nextThreshold = entry.requiredXp; + break; + } + + boolean isAtMax = currentLevel >= maxLevel; + + if (isAtMax) { + nextThreshold = currentThreshold; + } + + int progressXp = Math.max(0, currentXp - currentThreshold); + int progressPercent; + + if (isAtMax) { + progressPercent = 100; + } else { + int delta = Math.max(0, nextThreshold - currentThreshold); + progressPercent = (delta <= 0) ? 100 : Math.max(0, Math.min(100, (int) Math.floor((progressXp * 100D) / delta))); + } + + int totalXpRequired = isAtMax ? currentThreshold : nextThreshold; + int xpRemaining = Math.max(0, totalXpRequired - currentXp); + + return new LevelProgress(currentLevel, currentXp, progressXp, progressPercent, totalXpRequired, xpRemaining, isAtMax, maxLevel); + } + + private static List buildThresholdEntries(WiredExtraVariableLevelUpSystem levelSystem) { + return switch (levelSystem.getMode()) { + case WiredExtraVariableLevelUpSystem.MODE_EXPONENTIAL -> buildExponentialEntries(levelSystem); + case WiredExtraVariableLevelUpSystem.MODE_MANUAL -> buildManualEntries(levelSystem); + default -> buildLinearEntries(levelSystem); + }; + } + + private static List buildLinearEntries(WiredExtraVariableLevelUpSystem levelSystem) { + List entries = new ArrayList<>(); + int maxLevel = Math.max(1, levelSystem.getMaxLevel()); + int stepSize = Math.max(0, levelSystem.getStepSize()); + + for (int level = 1; level <= maxLevel; level++) { + entries.add(new LevelEntry(level, clamp((long) (level - 1) * stepSize))); + } + + return entries; + } + + private static List buildExponentialEntries(WiredExtraVariableLevelUpSystem levelSystem) { + List entries = new ArrayList<>(); + int maxLevel = Math.max(1, levelSystem.getMaxLevel()); + int currentIncrement = Math.max(0, levelSystem.getFirstLevelXp()); + int factor = Math.max(0, levelSystem.getIncreaseFactor()); + long threshold = 0L; + + entries.add(new LevelEntry(1, 0)); + + for (int level = 2; level <= maxLevel; level++) { + threshold += currentIncrement; + entries.add(new LevelEntry(level, clamp(threshold))); + currentIncrement = clamp(Math.round(currentIncrement * (100D + factor) / 100D)); + } + + return entries; + } + + private static List buildManualEntries(WiredExtraVariableLevelUpSystem levelSystem) { + LinkedHashMap anchors = parseAnchors(levelSystem.getInterpolationText()); + if (!anchors.containsKey(1)) { + anchors.put(1, 0); + } + + List> sortedAnchors = new ArrayList<>(anchors.entrySet()); + sortedAnchors.sort(Map.Entry.comparingByKey()); + + if (sortedAnchors.isEmpty()) { + return Collections.singletonList(new LevelEntry(1, 0)); + } + + LinkedHashMap result = new LinkedHashMap<>(); + + for (int index = 0; index < sortedAnchors.size(); index++) { + Map.Entry current = sortedAnchors.get(index); + int currentLevel = Math.max(1, current.getKey()); + int currentXp = Math.max(0, current.getValue()); + + result.put(currentLevel, currentXp); + + if (index + 1 >= sortedAnchors.size()) { + continue; + } + + Map.Entry next = sortedAnchors.get(index + 1); + int nextLevel = Math.max(currentLevel, next.getKey()); + int nextXp = Math.max(0, next.getValue()); + + if (nextLevel <= currentLevel) { + continue; + } + + for (int level = currentLevel + 1; level < nextLevel; level++) { + double ratio = (double) (level - currentLevel) / (double) (nextLevel - currentLevel); + int interpolatedXp = clamp(Math.round(currentXp + ((nextXp - currentXp) * ratio))); + result.put(level, interpolatedXp); + } + } + + List entries = new ArrayList<>(); + for (Map.Entry entry : result.entrySet()) { + entries.add(new LevelEntry(entry.getKey(), entry.getValue())); + } + + entries.sort(Comparator.comparingInt(levelEntry -> levelEntry.level)); + return entries; + } + + private static LinkedHashMap parseAnchors(String interpolationText) { + LinkedHashMap result = new LinkedHashMap<>(); + if (interpolationText == null || interpolationText.trim().isEmpty()) { + return result; + } + + for (String rawLine : interpolationText.split("\n")) { + if (rawLine == null) { + continue; + } + + String line = rawLine.trim(); + if (line.isEmpty()) { + continue; + } + + int separatorIndex = line.indexOf('='); + if (separatorIndex < 0) { + separatorIndex = line.indexOf(','); + } + + if (separatorIndex <= 0) { + continue; + } + + Integer level = parseInteger(line.substring(0, separatorIndex)); + Integer xp = parseInteger(line.substring(separatorIndex + 1)); + + if (level == null || xp == null || level <= 0 || xp < 0) { + continue; + } + + result.put(level, xp); + } + + return result; + } + + private static Integer parseInteger(String value) { + try { + return (value == null || value.trim().isEmpty()) ? null : Integer.parseInt(value.trim()); + } catch (NumberFormatException ignored) { + return null; + } + } + + private static int clamp(long value) { + if (value > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + + if (value < Integer.MIN_VALUE) { + return Integer.MIN_VALUE; + } + + return (int) value; + } + + public static class DerivedDefinition { + private final int syntheticItemId; + private final int baseDefinitionItemId; + private final int subvariableType; + private final String variableName; + private final WiredVariableDefinitionInfo baseDefinition; + private final WiredExtraVariableLevelUpSystem levelSystem; + + public DerivedDefinition(int syntheticItemId, int baseDefinitionItemId, int subvariableType, String variableName, WiredVariableDefinitionInfo baseDefinition, WiredExtraVariableLevelUpSystem levelSystem) { + this.syntheticItemId = syntheticItemId; + this.baseDefinitionItemId = baseDefinitionItemId; + this.subvariableType = subvariableType; + this.variableName = variableName; + this.baseDefinition = baseDefinition; + this.levelSystem = levelSystem; + } + + public int getBaseDefinitionItemId() { + return this.baseDefinitionItemId; + } + + public int getSubvariableType() { + return this.subvariableType; + } + + public WiredVariableDefinitionInfo getBaseDefinition() { + return this.baseDefinition; + } + + public WiredExtraVariableLevelUpSystem getLevelSystem() { + return this.levelSystem; + } + } + + public static class LevelEntry { + private final int level; + private final int requiredXp; + + public LevelEntry(int level, int requiredXp) { + this.level = level; + this.requiredXp = requiredXp; + } + + public int getLevel() { + return this.level; + } + + public int getRequiredXp() { + return this.requiredXp; + } + } + + private static class LevelProgress { + private final int currentLevel; + private final int currentXp; + private final int progressXp; + private final int progressPercent; + private final int totalXpRequired; + private final int xpRemaining; + private final boolean isAtMax; + private final int maxLevel; + + private LevelProgress(int currentLevel, int currentXp, int progressXp, int progressPercent, int totalXpRequired, int xpRemaining, boolean isAtMax, int maxLevel) { + this.currentLevel = currentLevel; + this.currentXp = currentXp; + this.progressXp = progressXp; + this.progressPercent = progressPercent; + this.totalXpRequired = totalXpRequired; + this.xpRemaining = xpRemaining; + this.isAtMax = isAtMax; + this.maxLevel = maxLevel; + } + } + + private static class DecodedSyntheticId { + private final int targetType; + private final int baseDefinitionItemId; + private final int subvariableType; + + private DecodedSyntheticId(int targetType, int baseDefinitionItemId, int subvariableType) { + this.targetType = targetType; + this.baseDefinitionItemId = baseDefinitionItemId; + this.subvariableType = subvariableType; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java new file mode 100644 index 00000000..38764d79 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredVariableTextConnectorSupport.java @@ -0,0 +1,100 @@ +package com.eu.habbo.habbohotel.wired.core; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredExtraVariableTextConnector; +import com.eu.habbo.habbohotel.rooms.Room; +import gnu.trove.set.hash.THashSet; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public final class WiredVariableTextConnectorSupport { + private WiredVariableTextConnectorSupport() { + } + + public static boolean isTextConnected(Room room, InteractionWiredExtra definition) { + return getConnector(room, definition) != null; + } + + public static boolean isTextConnected(Room room, int definitionItemId) { + return getConnector(room, definitionItemId) != null; + } + + public static WiredExtraVariableTextConnector getConnector(Room room, int definitionItemId) { + List connectors = getConnectors(room, definitionItemId); + return connectors.isEmpty() ? null : connectors.get(0); + } + + public static List getConnectors(Room room, int definitionItemId) { + if (room == null || room.getRoomSpecialTypes() == null || definitionItemId <= 0) { + return Collections.emptyList(); + } + + InteractionWiredExtra extra = room.getRoomSpecialTypes().getExtra(definitionItemId); + return getConnectors(room, extra); + } + + public static WiredExtraVariableTextConnector getConnector(Room room, InteractionWiredExtra definition) { + List connectors = getConnectors(room, definition); + return connectors.isEmpty() ? null : connectors.get(0); + } + + public static List getConnectors(Room room, InteractionWiredExtra definition) { + if (room == null || definition == null || room.getRoomSpecialTypes() == null) { + return Collections.emptyList(); + } + + THashSet extras = room.getRoomSpecialTypes().getExtras(definition.getX(), definition.getY()); + if (extras == null || extras.isEmpty()) { + return Collections.emptyList(); + } + + List connectors = new ArrayList<>(); + + for (InteractionWiredExtra extra : WiredExecutionOrderUtil.sort(extras)) { + if (extra instanceof WiredExtraVariableTextConnector) { + connectors.add((WiredExtraVariableTextConnector) extra); + } + } + + return connectors; + } + + public static String toText(Room room, int definitionItemId, Integer value) { + if (value == null) { + return ""; + } + + for (WiredExtraVariableTextConnector connector : getConnectors(room, definitionItemId)) { + Map mappings = connector.getMappings(); + if (mappings.containsKey(value)) { + String mappedValue = mappings.get(value); + return mappedValue != null ? mappedValue : String.valueOf(value); + } + } + + return String.valueOf(value); + } + + public static Integer toValue(Room room, int definitionItemId, String text) { + if (text == null) { + return null; + } + + String normalizedText = text.trim(); + if (normalizedText.isEmpty()) { + return null; + } + + for (WiredExtraVariableTextConnector connector : getConnectors(room, definitionItemId)) { + Integer mappedValue = connector.resolveValue(normalizedText); + if (mappedValue != null) { + return mappedValue; + } + } + + return null; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/highscores/WiredHighscoreManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/highscores/WiredHighscoreManager.java index f00380ff..f39363d0 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/highscores/WiredHighscoreManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/highscores/WiredHighscoreManager.java @@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.wired.highscores; import com.eu.habbo.Emulator; import com.eu.habbo.plugin.EventHandler; import com.eu.habbo.plugin.events.emulator.EmulatorLoadedEvent; +import com.eu.habbo.util.HotelDateTimeUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,9 +12,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.time.DayOfWeek; -import java.time.LocalDateTime; import java.time.LocalTime; -import java.time.ZoneId; import java.time.temporal.TemporalAdjusters; import java.time.temporal.WeekFields; import java.util.*; @@ -31,8 +30,6 @@ public class WiredHighscoreManager { private final static DayOfWeek firstDayOfWeek = WeekFields.of(Locale.of(locale, country)).getFirstDayOfWeek(); private final static DayOfWeek lastDayOfWeek = DayOfWeek.of(((firstDayOfWeek.getValue() + 5) % DayOfWeek.values().length) + 1); - private final static ZoneId zoneId = ZoneId.systemDefault(); - public static ScheduledFuture midnightUpdater = null; public void load() { @@ -183,26 +180,26 @@ public class WiredHighscoreManager { } private long getTodayStartTimestamp() { - return LocalDateTime.now().with(LocalTime.MIDNIGHT).atZone(zoneId).toEpochSecond(); + return HotelDateTimeUtil.toEpochSecond(HotelDateTimeUtil.localDateTimeNow().with(LocalTime.MIDNIGHT)); } private long getTodayEndTimestamp() { - return LocalDateTime.now().with(LocalTime.MIDNIGHT).plusDays(1).plusSeconds(-1).atZone(zoneId).toEpochSecond(); + return HotelDateTimeUtil.toEpochSecond(HotelDateTimeUtil.localDateTimeNow().with(LocalTime.MIDNIGHT).plusDays(1).plusSeconds(-1)); } private long getWeekStartTimestamp() { - return LocalDateTime.now().with(LocalTime.MIDNIGHT).with(TemporalAdjusters.previousOrSame(firstDayOfWeek)).atZone(zoneId).toEpochSecond(); + return HotelDateTimeUtil.toEpochSecond(HotelDateTimeUtil.localDateTimeNow().with(LocalTime.MIDNIGHT).with(TemporalAdjusters.previousOrSame(firstDayOfWeek))); } private long getWeekEndTimestamp() { - return LocalDateTime.now().with(LocalTime.MIDNIGHT).plusDays(1).plusSeconds(-1).with(TemporalAdjusters.nextOrSame(lastDayOfWeek)).atZone(zoneId).toEpochSecond(); + return HotelDateTimeUtil.toEpochSecond(HotelDateTimeUtil.localDateTimeNow().with(LocalTime.MIDNIGHT).plusDays(1).plusSeconds(-1).with(TemporalAdjusters.nextOrSame(lastDayOfWeek))); } private long getMonthStartTimestamp() { - return LocalDateTime.now().with(LocalTime.MIDNIGHT).with(TemporalAdjusters.firstDayOfMonth()).atZone(zoneId).toEpochSecond(); + return HotelDateTimeUtil.toEpochSecond(HotelDateTimeUtil.localDateTimeNow().with(LocalTime.MIDNIGHT).with(TemporalAdjusters.firstDayOfMonth())); } private long getMonthEndTimestamp() { - return LocalDateTime.now().with(LocalTime.MIDNIGHT).plusDays(1).plusSeconds(-1).with(TemporalAdjusters.lastDayOfMonth()).atZone(zoneId).toEpochSecond(); + return HotelDateTimeUtil.toEpochSecond(HotelDateTimeUtil.localDateTimeNow().with(LocalTime.MIDNIGHT).plusDays(1).plusSeconds(-1).with(TemporalAdjusters.lastDayOfMonth())); } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/highscores/WiredHighscoreMidnightUpdater.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/highscores/WiredHighscoreMidnightUpdater.java index 7b60b22c..bb53dd4b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/highscores/WiredHighscoreMidnightUpdater.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/highscores/WiredHighscoreMidnightUpdater.java @@ -4,11 +4,10 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredHighscore; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.util.HotelDateTimeUtil; import gnu.trove.set.hash.THashSet; -import java.time.LocalDateTime; import java.time.LocalTime; -import java.time.ZoneId; import java.util.List; public class WiredHighscoreMidnightUpdater implements Runnable { @@ -30,6 +29,7 @@ public class WiredHighscoreMidnightUpdater implements Runnable { } public static int getNextUpdaterRun() { - return Math.toIntExact(LocalDateTime.now().with(LocalTime.MIDNIGHT).plusDays(1).plusSeconds(-1).atZone(ZoneId.systemDefault()).toEpochSecond() - Emulator.getIntUnixTimestamp()) + 5; + long nextRunTimestamp = HotelDateTimeUtil.toEpochSecond(HotelDateTimeUtil.localDateTimeNow().with(LocalTime.MIDNIGHT).plusDays(1).plusSeconds(-1)); + return Math.toIntExact(nextRunTimestamp - Emulator.getIntUnixTimestamp()) + 5; } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/migrate/WiredEvents.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/migrate/WiredEvents.java index 91bba744..003e06a2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/migrate/WiredEvents.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/migrate/WiredEvents.java @@ -67,6 +67,70 @@ public final class WiredEvents { .build(); } + /** + * Create an event for when a user clicks furniture. + * @param room the room + * @param user the clicking user + * @param item the clicked furniture + * @return the event + */ + public static WiredEvent userClicksFurni(Room room, RoomUnit user, HabboItem item) { + RoomTile tile = room.getLayout().getTile(item.getX(), item.getY()); + return WiredEvent.builder(WiredEvent.Type.USER_CLICKS_FURNI, room) + .actor(user) + .sourceItem(item) + .tile(tile) + .build(); + } + + /** + * Create an event for when a user clicks invisible click tile furniture. + * @param room the room + * @param user the clicking user + * @param item the clicked furniture + * @return the event + */ + public static WiredEvent userClicksTile(Room room, RoomUnit user, HabboItem item) { + RoomTile tile = room.getLayout().getTile(item.getX(), item.getY()); + return WiredEvent.builder(WiredEvent.Type.USER_CLICKS_TILE, room) + .actor(user) + .sourceItem(item) + .tile(tile) + .build(); + } + + /** + * Create an event for when a user clicks another user. + * @param room the room + * @param clickingUser the user performing the click + * @param clickedUser the user who was clicked + * @return the event + */ + public static WiredEvent userClicksUser(Room room, RoomUnit clickingUser, RoomUnit clickedUser) { + return WiredEvent.builder(WiredEvent.Type.USER_CLICKS_USER, room) + .actor(clickingUser) + .targetUnit(clickedUser) + .tile(clickedUser.getCurrentLocation()) + .build(); + } + + /** + * Create an event for when a user performs an avatar action. + * @param room the room + * @param user the acting user + * @param actionId the wired action id + * @param actionParameter sign/dance parameter, or -1 when unused + * @return the event + */ + public static WiredEvent userPerformsAction(Room room, RoomUnit user, int actionId, int actionParameter) { + return WiredEvent.builder(WiredEvent.Type.USER_PERFORMS_ACTION, room) + .actor(user) + .tile(user.getCurrentLocation()) + .actionId(actionId) + .actionParameter(actionParameter) + .build(); + } + /** * Create an event for when a user enters the room. * @param room the room @@ -80,6 +144,19 @@ public final class WiredEvents { .build(); } + /** + * Create an event for when a user leaves the room. + * @param room the room + * @param user the user who left + * @return the event + */ + public static WiredEvent userLeavesRoom(Room room, RoomUnit user) { + return WiredEvent.builder(WiredEvent.Type.USER_LEAVES_ROOM, room) + .actor(user) + .tile(user.getCurrentLocation()) + .build(); + } + // ========== User Interaction Events ========== /** @@ -90,9 +167,15 @@ public final class WiredEvents { * @return the event */ public static WiredEvent userSays(Room room, RoomUnit user, String message) { + return userSays(room, user, message, -1, -1); + } + + public static WiredEvent userSays(Room room, RoomUnit user, String message, int chatType, int chatStyle) { return WiredEvent.builder(WiredEvent.Type.USER_SAYS, room) .actor(user) .text(message) + .chatType(chatType) + .chatStyle(chatStyle) .tile(user.getCurrentLocation()) .build(); } @@ -115,6 +198,42 @@ public final class WiredEvents { .build(); } + public static WiredEvent userVariableChanged(Room room, RoomUnit user, int definitionItemId, boolean created, boolean deleted, WiredEvent.VariableChangeKind changeKind) { + return WiredEvent.builder(WiredEvent.Type.VARIABLE_CHANGED, room) + .actor(user) + .tile((user != null) ? user.getCurrentLocation() : null) + .variableTargetType(0) + .variableDefinitionItemId(definitionItemId) + .variableCreated(created) + .variableDeleted(deleted) + .variableChangeKind(changeKind) + .build(); + } + + public static WiredEvent furniVariableChanged(Room room, HabboItem item, int definitionItemId, boolean created, boolean deleted, WiredEvent.VariableChangeKind changeKind) { + RoomTile tile = (item != null) ? room.getLayout().getTile(item.getX(), item.getY()) : null; + + return WiredEvent.builder(WiredEvent.Type.VARIABLE_CHANGED, room) + .sourceItem(item) + .tile(tile) + .variableTargetType(1) + .variableDefinitionItemId(definitionItemId) + .variableCreated(created) + .variableDeleted(deleted) + .variableChangeKind(changeKind) + .build(); + } + + public static WiredEvent roomVariableChanged(Room room, int definitionItemId, WiredEvent.VariableChangeKind changeKind) { + return WiredEvent.builder(WiredEvent.Type.VARIABLE_CHANGED, room) + .variableTargetType(3) + .variableDefinitionItemId(definitionItemId) + .variableCreated(false) + .variableDeleted(false) + .variableChangeKind(changeKind) + .build(); + } + // ========== Timer Events ========== /** @@ -141,6 +260,12 @@ public final class WiredEvents { .build(); } + public static WiredEvent clockCounter(Room room, HabboItem counterItem) { + return WiredEvent.builder(WiredEvent.Type.CLOCK_COUNTER_REACHED, room) + .sourceItem(counterItem) + .build(); + } + /** * Create an event for a long periodic timer. * @param room the room @@ -153,6 +278,18 @@ public final class WiredEvents { .build(); } + /** + * Create an event for a short periodic timer. + * @param room the room + * @param timerItem the timer furniture + * @return the event + */ + public static WiredEvent timerRepeatShort(Room room, HabboItem timerItem) { + return WiredEvent.builder(WiredEvent.Type.TIMER_REPEAT_SHORT, room) + .sourceItem(timerItem) + .build(); + } + // ========== Game Events ========== /** diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/tick/WiredTickService.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/tick/WiredTickService.java index 144f73d7..070718dd 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/tick/WiredTickService.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/tick/WiredTickService.java @@ -9,133 +9,110 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; /** * Centralized tick service for all wired timing operations. - *

- * This service runs a single 50ms tick loop that processes all registered - * {@link WiredTickable} items across all rooms. This replaces the old - * per-room 500ms cycle approach and provides: - *

- * - *
    - *
  • Higher resolution timing (50ms vs 500ms)
  • - *
  • Centralized management - single thread for all rooms
  • - *
  • Proper room lifecycle handling
  • - *
  • Efficient registration/unregistration
  • - *
- * - *

Architecture:

- *
- * WiredTickService (singleton)
- *   └── ScheduledExecutorService (50ms tick)
- *         └── For each room with tickables:
- *               └── For each WiredTickable:
- *                     └── onWiredTick(room, currentTime)
- * 
- * - *

Thread Safety:

- * All collections are thread-safe. The tick loop catches and logs exceptions - * to prevent one bad item from crashing the entire service. - * - * @see WiredTickable + * + *

This version keeps a single global tick clock, but distributes room processing + * across multiple single-threaded shard workers. A room is always processed on the + * same shard, preserving in-room order while preventing one heavy room from delaying + * all other rooms.

*/ public final class WiredTickService { - + private static final Logger LOGGER = LoggerFactory.getLogger(WiredTickService.class); - - /** Default tick interval in milliseconds */ + public static final int DEFAULT_TICK_INTERVAL_MS = 50; - - /** Minimum allowed tick interval (prevents CPU overload) */ public static final int MIN_TICK_INTERVAL_MS = 10; - - /** Maximum allowed tick interval */ public static final int MAX_TICK_INTERVAL_MS = 500; - - /** Singleton instance */ + + public static final int DEFAULT_WORKER_COUNT = Math.max(2, Math.min(8, Runtime.getRuntime().availableProcessors())); + public static final int MIN_WORKER_COUNT = 1; + public static final int MAX_WORKER_COUNT = 32; + + public static final long SLOW_TICKABLE_THRESHOLD_MS = 100L; + public static final long SLOW_ROOM_THRESHOLD_MS = 50L; + public static final long SLOW_SHARD_THRESHOLD_MS = 250L; + private static volatile WiredTickService instance; - - /** The configured tick interval in milliseconds */ + private int tickIntervalMs = DEFAULT_TICK_INTERVAL_MS; - - /** Whether debug logging is enabled */ private boolean debugEnabled = false; - - /** Thread priority for the tick service */ private int threadPriority = Thread.NORM_PRIORITY + 1; - - /** - * Global tick counter - increments every tick. - * All repeaters use this to stay synchronized. - * Repeaters fire when (tickCount * tickIntervalMs) % repeatTime == 0 - */ - private volatile long tickCount = 0; - - /** The scheduled executor for the tick loop */ - private ScheduledExecutorService scheduler; - - /** The scheduled future for the tick task */ - private ScheduledFuture tickTask; - - /** Map of room ID to set of registered tickables */ + private int workerCount = DEFAULT_WORKER_COUNT; + + /** Global logical tick counter shared by every shard. */ + private final AtomicLong tickCount = new AtomicLong(0); + + /** Schedules the global logical ticks. */ + private ScheduledExecutorService coordinator; + + /** One single-thread executor per shard, preserving order inside the shard. */ + private ExecutorService[] shardExecutors; + + /** Highest logical tick requested for each shard. */ + private AtomicLong[] shardRequestedTicks; + + /** Last logical tick fully processed by each shard. */ + private AtomicLong[] shardProcessedTicks; + + /** Whether a shard worker loop is currently scheduled/running. */ + private AtomicBoolean[] shardScheduled; + private final ConcurrentHashMap> roomTickables; - - /** Whether the service is running */ private final AtomicBoolean running; - - /** - * Private constructor for singleton. - */ + private WiredTickService() { this.roomTickables = new ConcurrentHashMap<>(); this.running = new AtomicBoolean(false); } - - /** - * Loads configuration from emulator settings. - */ + private void loadConfiguration() { - // Load tick interval int configuredInterval = Emulator.getConfig().getInt("wired.tick.interval.ms", DEFAULT_TICK_INTERVAL_MS); this.tickIntervalMs = Math.max(MIN_TICK_INTERVAL_MS, Math.min(MAX_TICK_INTERVAL_MS, configuredInterval)); - + if (configuredInterval != this.tickIntervalMs) { - LOGGER.warn("wired.tick.interval.ms value {} is out of range [{}-{}], using {}", - configuredInterval, MIN_TICK_INTERVAL_MS, MAX_TICK_INTERVAL_MS, this.tickIntervalMs); + LOGGER.warn( + "wired.tick.interval.ms value {} is out of range [{}-{}], using {}", + configuredInterval, + MIN_TICK_INTERVAL_MS, + MAX_TICK_INTERVAL_MS, + this.tickIntervalMs + ); } - - // Load debug flag + this.debugEnabled = Emulator.getConfig().getBoolean("wired.tick.debug", false); - - // Load thread priority + int configuredPriority = Emulator.getConfig().getInt("wired.tick.thread.priority", Thread.NORM_PRIORITY + 1); this.threadPriority = Math.max(Thread.MIN_PRIORITY, Math.min(Thread.MAX_PRIORITY, configuredPriority)); + + int configuredWorkers = Emulator.getConfig().getInt("wired.tick.workers", DEFAULT_WORKER_COUNT); + this.workerCount = Math.max(MIN_WORKER_COUNT, Math.min(MAX_WORKER_COUNT, configuredWorkers)); + + if (configuredWorkers != this.workerCount) { + LOGGER.warn( + "wired.tick.workers value {} is out of range [{}-{}], using {}", + configuredWorkers, + MIN_WORKER_COUNT, + MAX_WORKER_COUNT, + this.workerCount + ); + } } - - /** - * Gets the configured tick interval in milliseconds. - * - * @return the tick interval - */ + public int getTickIntervalMs() { return tickIntervalMs; } - - /** - * Checks if debug logging is enabled. - * - * @return true if debug is enabled - */ + public boolean isDebugEnabled() { return debugEnabled; } - - /** - * Gets the singleton instance. - * - * @return the WiredTickService instance - */ + + public int getWorkerCount() { + return workerCount; + } + public static WiredTickService getInstance() { if (instance == null) { synchronized (WiredTickService.class) { @@ -146,150 +123,158 @@ public final class WiredTickService { } return instance; } - - /** - * Starts the tick service. - *

- * Should be called during emulator startup after WiredManager.initialize(). - *

- */ + public synchronized void start() { if (running.get()) { LOGGER.warn("WiredTickService already running"); return; } - - // Load configuration from emulator settings + loadConfiguration(); - - LOGGER.info("Starting WiredTickService with {}ms tick interval (debug={}, priority={})...", - tickIntervalMs, debugEnabled, threadPriority); - - this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> { - Thread t = new Thread(r, "WiredTickService"); + + LOGGER.info( + "Starting WiredTickService with {}ms tick interval (workers={}, debug={}, priority={})...", + tickIntervalMs, + workerCount, + debugEnabled, + threadPriority + ); + + this.coordinator = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "WiredTickCoordinator"); t.setDaemon(true); t.setPriority(threadPriority); return t; }); - - this.tickTask = scheduler.scheduleAtFixedRate( - this::tick, - tickIntervalMs, - tickIntervalMs, - TimeUnit.MILLISECONDS - ); - + + this.shardExecutors = new ExecutorService[workerCount]; + this.shardRequestedTicks = new AtomicLong[workerCount]; + this.shardProcessedTicks = new AtomicLong[workerCount]; + this.shardScheduled = new AtomicBoolean[workerCount]; + + for (int i = 0; i < workerCount; i++) { + final int shardIndex = i; + this.shardExecutors[i] = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "WiredTickShard-" + shardIndex); + t.setDaemon(true); + t.setPriority(threadPriority); + return t; + }); + this.shardRequestedTicks[i] = new AtomicLong(0L); + this.shardProcessedTicks[i] = new AtomicLong(0L); + this.shardScheduled[i] = new AtomicBoolean(false); + } + + this.tickCount.set(0L); running.set(true); + + this.coordinator.scheduleAtFixedRate( + () -> { + try { + dispatchTick(); + } catch (Throwable t) { + LOGGER.error("WiredTickService fatal coordinator error", t); + } + }, + tickIntervalMs, + tickIntervalMs, + TimeUnit.MILLISECONDS + ); + LOGGER.info("WiredTickService started successfully"); } - - /** - * Stops the tick service. - *

- * Should be called during emulator shutdown. - *

- */ + public synchronized void stop() { if (!running.get()) { return; } - + LOGGER.info("Stopping WiredTickService..."); - running.set(false); - - if (tickTask != null) { - tickTask.cancel(false); - tickTask = null; - } - - if (scheduler != null) { - scheduler.shutdown(); + + if (coordinator != null) { + coordinator.shutdown(); try { - if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { - scheduler.shutdownNow(); + if (!coordinator.awaitTermination(5, TimeUnit.SECONDS)) { + coordinator.shutdownNow(); } } catch (InterruptedException e) { - scheduler.shutdownNow(); + coordinator.shutdownNow(); Thread.currentThread().interrupt(); } - scheduler = null; + coordinator = null; } - + + if (shardExecutors != null) { + for (ExecutorService executor : shardExecutors) { + if (executor != null) { + executor.shutdown(); + } + } + + for (ExecutorService executor : shardExecutors) { + if (executor == null) { + continue; + } + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + shardExecutors = null; + shardRequestedTicks = null; + shardProcessedTicks = null; + shardScheduled = null; + roomTickables.clear(); LOGGER.info("WiredTickService stopped"); } - - /** - * Checks if the service is running. - * - * @return true if running - */ + public boolean isRunning() { return running.get(); } - - /** - * Registers a tickable item with the service. - *

- * The item will start receiving {@link WiredTickable#onWiredTick} calls - * on the next tick cycle. - *

- * - * @param room the room the item is in - * @param tickable the tickable item - */ + public void register(Room room, WiredTickable tickable) { if (room == null || tickable == null) { return; } - + int roomId = room.getId(); - Set tickables = roomTickables.computeIfAbsent( - roomId, - k -> ConcurrentHashMap.newKeySet() - ); - + Set tickables = roomTickables.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet()); + if (tickables.add(tickable)) { tickable.onRegistered(room, System.currentTimeMillis()); } } - - /** - * Unregisters a tickable item from the service. - * - * @param room the room the item was in - * @param tickable the tickable item - */ + public void unregister(Room room, WiredTickable tickable) { if (room == null || tickable == null) { return; } - + int roomId = room.getId(); Set tickables = roomTickables.get(roomId); - + if (tickables != null) { if (tickables.remove(tickable)) { tickable.onUnregistered(room); } - - // Clean up empty sets + if (tickables.isEmpty()) { roomTickables.remove(roomId); } } } - - /** - * Unregisters a tickable by ID. - * - * @param roomId the room ID - * @param tickableId the tickable item ID - */ + public void unregister(int roomId, int tickableId) { Set tickables = roomTickables.get(roomId); - + if (tickables != null) { tickables.removeIf(t -> { if (t.getId() == tickableId) { @@ -301,162 +286,240 @@ public final class WiredTickService { } return false; }); - + if (tickables.isEmpty()) { roomTickables.remove(roomId); } } } - - /** - * Unregisters all tickables for a room. - *

- * Should be called when a room is unloaded. - *

- * - * @param room the room - */ + public void unregisterRoom(Room room) { if (room == null) { return; } - + Set tickables = roomTickables.remove(room.getId()); - + if (tickables != null) { - for (WiredTickable tickable : tickables) { - tickable.onUnregistered(room); + WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]); + for (WiredTickable tickable : snapshot) { + try { + if (tickable != null) { + tickable.onUnregistered(room); + } + } catch (Throwable t) { + LOGGER.error( + "Error unregistering tickable {} from room {}", + tickable != null ? tickable.getId() : -1, + room.getId(), + t + ); + } } - LOGGER.debug("Unregistered {} tickables from room {}", tickables.size(), room.getId()); + LOGGER.debug("Unregistered {} tickables from room {}", snapshot.length, room.getId()); } } - - /** - * Resets all timers in a room. - * - * @param room the room - */ + public void resetRoomTimers(Room room) { if (room == null) { return; } - + Set tickables = roomTickables.get(room.getId()); - + if (tickables != null) { - for (WiredTickable tickable : tickables) { + WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]); + for (WiredTickable tickable : snapshot) { try { - tickable.resetTimer(); - } catch (Exception e) { - LOGGER.error("Error resetting timer for tickable {} in room {}", - tickable.getId(), room.getId(), e); + if (tickable != null) { + tickable.resetTimer(); + } + } catch (Throwable e) { + LOGGER.error( + "Error resetting timer for tickable {} in room {}", + tickable != null ? tickable.getId() : -1, + room.getId(), + e + ); } } } } - - /** - * Gets the count of registered tickables for a room. - * - * @param roomId the room ID - * @return the count - */ + public int getTickableCount(int roomId) { Set tickables = roomTickables.get(roomId); return tickables != null ? tickables.size() : 0; } - - /** - * Gets the total count of registered tickables across all rooms. - * - * @return the total count - */ + public int getTotalTickableCount() { - return roomTickables.values().stream() - .mapToInt(Set::size) - .sum(); + return roomTickables.values().stream().mapToInt(Set::size).sum(); } - - /** - * Gets the count of rooms with registered tickables. - * - * @return the room count - */ + public int getActiveRoomCount() { return roomTickables.size(); } - - /** - * The main tick loop. - *

- * Called at the configured interval by the scheduler. Processes all registered tickables - * across all rooms. - *

- */ - private void tick() { + + public long getTickCount() { + return tickCount.get(); + } + + private void dispatchTick() { if (!running.get() || Emulator.isShuttingDown) { return; } - - // Increment global tick counter - tickCount++; - - long startTime = System.currentTimeMillis(); - int tickablesProcessed = 0; - + + long currentTick = tickCount.incrementAndGet(); + + for (int shardIndex = 0; shardIndex < workerCount; shardIndex++) { + shardRequestedTicks[shardIndex].set(currentTick); + scheduleShardIfNeeded(shardIndex); + } + } + + private void scheduleShardIfNeeded(int shardIndex) { + if (!running.get() || shardExecutors == null) { + return; + } + + if (shardScheduled[shardIndex].compareAndSet(false, true)) { + shardExecutors[shardIndex].execute(() -> runShardLoop(shardIndex)); + } + } + + private void runShardLoop(int shardIndex) { + try { + while (running.get() && !Emulator.isShuttingDown) { + long nextTick = shardProcessedTicks[shardIndex].get() + 1L; + long requestedTick = shardRequestedTicks[shardIndex].get(); + + if (nextTick > requestedTick) { + break; + } + + processShardTick(shardIndex, nextTick); + shardProcessedTicks[shardIndex].set(nextTick); + } + } catch (Throwable t) { + LOGGER.error("Fatal error in WiredTick shard {}", shardIndex, t); + } finally { + shardScheduled[shardIndex].set(false); + if (running.get() && shardProcessedTicks[shardIndex].get() < shardRequestedTicks[shardIndex].get()) { + scheduleShardIfNeeded(shardIndex); + } + } + } + + private void processShardTick(int shardIndex, long currentTick) { + long shardStart = System.currentTimeMillis(); + int processedTickables = 0; + int processedRooms = 0; + for (Map.Entry> entry : roomTickables.entrySet()) { int roomId = entry.getKey(); - Set tickables = entry.getValue(); - - if (tickables.isEmpty()) { + if (getShardIndex(roomId) != shardIndex) { continue; } - - // Get the room - skip if not loaded + + Set tickables = entry.getValue(); + if (tickables == null || tickables.isEmpty()) { + continue; + } + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId); if (room == null || !room.isLoaded()) { continue; } - - // Skip if room is empty (optimization) + if (room.getCurrentHabbos().isEmpty() && room.getCurrentBots().isEmpty()) { continue; } - - // Process each tickable - for (WiredTickable tickable : tickables) { + + long roomStart = System.currentTimeMillis(); + WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]); + if (snapshot.length == 0) { + continue; + } + + processedRooms++; + + for (WiredTickable tickable : snapshot) { + long tickableStart = System.currentTimeMillis(); + + if (tickable == null) { + continue; + } + try { - // Verify item still belongs to this room if (tickable.getRoomId() != roomId) { - // Item moved to another room, unregister it - tickables.remove(tickable); + unregister(roomId, tickable.getId()); continue; } - - // Pass global tick count - all tickables see the same counter - // This keeps repeaters with the same interval perfectly synchronized - tickable.onWiredTick(room, tickCount, tickIntervalMs); - tickablesProcessed++; - } catch (Exception e) { - LOGGER.error("Error in wired tick for tickable {} in room {}: {}", - tickable.getId(), roomId, e.getMessage(), e); + + tickable.onWiredTick(room, currentTick, tickIntervalMs); + processedTickables++; + + long tickableDuration = System.currentTimeMillis() - tickableStart; + if (tickableDuration > SLOW_TICKABLE_THRESHOLD_MS) { + LOGGER.warn( + "Slow wired tickable: shard={}, room={}, tick={}, tickableId={}, class={}, took={}ms", + shardIndex, + roomId, + currentTick, + tickable.getId(), + tickable.getClass().getName(), + tickableDuration + ); + } + } catch (Throwable t) { + long tickableDuration = System.currentTimeMillis() - tickableStart; + LOGGER.error( + "Error in wired tick for tickable {} in room {} after {}ms", + tickable.getId(), + roomId, + tickableDuration, + t + ); } } + + long roomDuration = System.currentTimeMillis() - roomStart; + if (roomDuration > SLOW_ROOM_THRESHOLD_MS) { + LOGGER.warn( + "Slow wired room tick: shard={}, room={}, tick={}, tickables={}, took={}ms", + shardIndex, + roomId, + currentTick, + snapshot.length, + roomDuration + ); + } } - - // Debug logging if enabled - if (debugEnabled && tickablesProcessed > 0) { - LOGGER.debug("Wired tick #{} completed: {} tickables processed in {}ms", - tickCount, tickablesProcessed, System.currentTimeMillis() - startTime); + + long shardDuration = System.currentTimeMillis() - shardStart; + if (shardDuration > SLOW_SHARD_THRESHOLD_MS) { + LOGGER.warn( + "Slow wired shard tick: shard={}, tick={}, rooms={}, tickables={}, took={}ms", + shardIndex, + currentTick, + processedRooms, + processedTickables, + shardDuration + ); + } + + if (debugEnabled && processedTickables > 0) { + LOGGER.debug( + "Wired shard tick completed: shard={}, tick={}, rooms={}, tickables={}, took={}ms", + shardIndex, + currentTick, + processedRooms, + processedTickables, + shardDuration + ); } } - - /** - * Gets the current global tick count. - * - * @return the tick count - */ - public long getTickCount() { - return tickCount; + + private int getShardIndex(int roomId) { + return Math.floorMod(roomId, workerCount); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index 6bd33db3..e1e4d312 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -10,6 +10,8 @@ import com.eu.habbo.messages.incoming.ambassadors.AmbassadorAlertCommandEvent; import com.eu.habbo.messages.incoming.ambassadors.AmbassadorVisitCommandEvent; import com.eu.habbo.messages.incoming.camera.*; import com.eu.habbo.messages.incoming.catalog.*; +import com.eu.habbo.messages.incoming.catalog.catalogadmin.*; +import com.eu.habbo.messages.incoming.furnieditor.*; import com.eu.habbo.messages.incoming.catalog.marketplace.*; import com.eu.habbo.messages.incoming.catalog.recycler.OpenRecycleBoxEvent; import com.eu.habbo.messages.incoming.catalog.recycler.RecycleEvent; @@ -34,6 +36,7 @@ import com.eu.habbo.messages.incoming.helper.MySanctionStatusEvent; import com.eu.habbo.messages.incoming.helper.RequestTalentTrackEvent; import com.eu.habbo.messages.incoming.hotelview.*; import com.eu.habbo.messages.incoming.inventory.*; +import com.eu.habbo.messages.incoming.inventory.prefixes.*; import com.eu.habbo.messages.incoming.modtool.*; import com.eu.habbo.messages.incoming.navigator.*; import com.eu.habbo.messages.incoming.polls.AnswerPollEvent; @@ -64,6 +67,13 @@ import com.eu.habbo.messages.incoming.users.*; import com.eu.habbo.messages.incoming.wired.WiredApplySetConditionsEvent; import com.eu.habbo.messages.incoming.wired.WiredConditionSaveDataEvent; import com.eu.habbo.messages.incoming.wired.WiredEffectSaveDataEvent; +import com.eu.habbo.messages.incoming.wired.WiredMonitorRequestEvent; +import com.eu.habbo.messages.incoming.wired.WiredRoomSettingsRequestEvent; +import com.eu.habbo.messages.incoming.wired.WiredRoomSettingsSaveEvent; +import com.eu.habbo.messages.incoming.wired.WiredUserInspectMoveEvent; +import com.eu.habbo.messages.incoming.wired.WiredUserVariableManageEvent; +import com.eu.habbo.messages.incoming.wired.WiredUserVariableUpdateEvent; +import com.eu.habbo.messages.incoming.wired.WiredUserVariablesRequestEvent; import com.eu.habbo.messages.incoming.wired.WiredTriggerSaveDataEvent; import com.eu.habbo.plugin.EventHandler; import com.eu.habbo.plugin.events.emulator.EmulatorConfigUpdatedEvent; @@ -230,7 +240,9 @@ public class PacketManager { this.registerHandler(Incoming.RequestGiftConfigurationEvent, RequestGiftConfigurationEvent.class); this.registerHandler(Incoming.GetMarketplaceConfigEvent, RequestMarketplaceConfigEvent.class); this.registerHandler(Incoming.RequestCatalogModeEvent, RequestCatalogModeEvent.class); - this.registerHandler(Incoming.RequestCatalogIndexEvent, RequestCatalogIndexEvent.class); + this.registerHandler(Incoming.BuildersClubQueryFurniCountEvent, BuildersClubQueryFurniCountEvent.class); + this.registerHandler(Incoming.BuildersClubPlaceRoomItemEvent, BuildersClubPlaceRoomItemEvent.class); + this.registerHandler(Incoming.BuildersClubPlaceWallItemEvent, BuildersClubPlaceWallItemEvent.class); this.registerHandler(Incoming.RequestCatalogPageEvent, RequestCatalogPageEvent.class); this.registerHandler(Incoming.CatalogBuyItemAsGiftEvent, CatalogBuyItemAsGiftEvent.class); this.registerHandler(Incoming.CatalogBuyItemEvent, CatalogBuyItemEvent.class); @@ -257,6 +269,27 @@ public class PacketManager { this.registerHandler(Incoming.RequestClubCenterEvent, RequestClubCenterEvent.class); this.registerHandler(Incoming.CatalogRequestClubDiscountEvent, CatalogRequestClubDiscountEvent.class); this.registerHandler(Incoming.CatalogBuyClubDiscountEvent, CatalogBuyClubDiscountEvent.class); + + // Furni Editor + this.registerHandler(Incoming.FurniEditorSearchEvent, FurniEditorSearchEvent.class); + this.registerHandler(Incoming.FurniEditorDetailEvent, FurniEditorDetailEvent.class); + this.registerHandler(Incoming.FurniEditorBySpriteEvent, FurniEditorBySpriteEvent.class); + this.registerHandler(Incoming.FurniEditorInteractionsEvent, FurniEditorInteractionsEvent.class); + this.registerHandler(Incoming.FurniEditorUpdateEvent, FurniEditorUpdateEvent.class); + this.registerHandler(Incoming.FurniEditorDeleteEvent, FurniEditorDeleteEvent.class); + + // Catalog Admin + this.registerHandler(Incoming.CatalogAdminSavePageEvent, CatalogAdminSavePageEvent.class); + this.registerHandler(Incoming.CatalogAdminCreatePageEvent, CatalogAdminCreatePageEvent.class); + this.registerHandler(Incoming.CatalogAdminDeletePageEvent, CatalogAdminDeletePageEvent.class); + this.registerHandler(Incoming.CatalogAdminSaveOfferEvent, CatalogAdminSaveOfferEvent.class); + this.registerHandler(Incoming.CatalogAdminCreateOfferEvent, CatalogAdminCreateOfferEvent.class); + this.registerHandler(Incoming.CatalogAdminDeleteOfferEvent, CatalogAdminDeleteOfferEvent.class); + this.registerHandler(Incoming.CatalogAdminMoveOfferEvent, CatalogAdminMoveOfferEvent.class); + this.registerHandler(Incoming.CatalogAdminMovePageEvent, CatalogAdminMovePageEvent.class); + this.registerHandler(Incoming.CatalogAdminPublishEvent, CatalogAdminPublishEvent.class); + this.registerHandler(Incoming.CatalogAdminSavePageImagesEvent, CatalogAdminSavePageImagesEvent.class); + this.registerHandler(Incoming.CatalogAdminSavePageIconEvent, CatalogAdminSavePageIconEvent.class); } private void registerEvent() throws Exception { @@ -370,6 +403,12 @@ public class PacketManager { this.registerHandler(Incoming.RequestInventoryPetsEvent, RequestInventoryPetsEvent.class); this.registerHandler(Incoming.RequestInventoryPetDelete, RequestInventoryPetDelete.class); this.registerHandler(Incoming.RequestInventoryBadgeDelete, RequestInventoryBadgeDelete.class); + + // Custom Prefixes + this.registerHandler(Incoming.RequestUserPrefixesEvent, RequestUserPrefixesEvent.class); + this.registerHandler(Incoming.SetActivePrefixEvent, SetActivePrefixEvent.class); + this.registerHandler(Incoming.DeletePrefixEvent, DeletePrefixEvent.class); + this.registerHandler(Incoming.PurchasePrefixEvent, PurchasePrefixEvent.class); } void registerRooms() throws Exception { @@ -387,6 +426,8 @@ public class PacketManager { this.registerHandler(Incoming.RoomPlacePaintEvent, RoomPlacePaintEvent.class); this.registerHandler(Incoming.RoomUserStartTypingEvent, RoomUserStartTypingEvent.class); this.registerHandler(Incoming.RoomUserStopTypingEvent, RoomUserStopTypingEvent.class); + this.registerHandler(Incoming.ClickFurniEvent, ClickFurniEvent.class); + this.registerHandler(Incoming.ClickUserEvent, ClickUserEvent.class); this.registerHandler(Incoming.ToggleFloorItemEvent, ToggleFloorItemEvent.class); this.registerHandler(Incoming.ToggleWallItemEvent, ToggleWallItemEvent.class); this.registerHandler(Incoming.RoomBackgroundEvent, RoomBackgroundEvent.class); @@ -551,6 +592,7 @@ public class PacketManager { this.registerHandler(Incoming.GuildForumModerateMessageEvent, GuildForumModerateMessageEvent.class); this.registerHandler(Incoming.GuildForumModerateThreadEvent, GuildForumModerateThreadEvent.class); this.registerHandler(Incoming.GuildForumThreadUpdateEvent, GuildForumThreadUpdateEvent.class); + this.registerHandler(Incoming.GuildForumMarkAsReadEvent, GuildForumMarkAsReadEvent.class); this.registerHandler(Incoming.GetHabboGuildBadgesMessageEvent, GetHabboGuildBadgesMessageEvent.class); // this.registerHandler(Incoming.GuildForumDataEvent, GuildForumModerateMessageEvent.class); @@ -584,6 +626,13 @@ public class PacketManager { this.registerHandler(Incoming.WiredEffectSaveDataEvent, WiredEffectSaveDataEvent.class); this.registerHandler(Incoming.WiredConditionSaveDataEvent, WiredConditionSaveDataEvent.class); this.registerHandler(Incoming.WiredApplySetConditionsEvent, WiredApplySetConditionsEvent.class); + this.registerHandler(Incoming.WiredMonitorRequestEvent, WiredMonitorRequestEvent.class); + this.registerHandler(Incoming.WiredRoomSettingsRequestEvent, WiredRoomSettingsRequestEvent.class); + this.registerHandler(Incoming.WiredRoomSettingsSaveEvent, WiredRoomSettingsSaveEvent.class); + this.registerHandler(Incoming.WiredUserVariablesRequestEvent, WiredUserVariablesRequestEvent.class); + this.registerHandler(Incoming.WiredUserVariableUpdateEvent, WiredUserVariableUpdateEvent.class); + this.registerHandler(Incoming.WiredUserVariableManageEvent, WiredUserVariableManageEvent.class); + this.registerHandler(Incoming.WiredUserInspectMoveEvent, WiredUserInspectMoveEvent.class); } void registerUnknown() throws Exception { @@ -646,5 +695,10 @@ public class PacketManager { this.registerHandler(Incoming.GameCenterLeaveGameEvent, GameCenterLeaveGameEvent.class); this.registerHandler(Incoming.GameCenterEvent, GameCenterEvent.class); this.registerHandler(Incoming.GameCenterRequestGameStatusEvent, GameCenterRequestGameStatusEvent.class); + + // YouTube Room Broadcast + this.registerHandler(Incoming.YouTubeRoomPlayEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomPlayEvent.class); + this.registerHandler(Incoming.YouTubeRoomWatchingEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomWatchingEvent.class); + this.registerHandler(Incoming.YouTubeRoomSettingsEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomSettingsEvent.class); } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index 8c9625e7..d2b24daf 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -84,6 +84,9 @@ public class Incoming { public static final int RequestRecylerLogicEvent = 398; public static final int RequestGuildJoinEvent = 998; public static final int RequestCatalogIndexEvent = 2529; + public static final int BuildersClubQueryFurniCountEvent = 2529; + public static final int BuildersClubPlaceRoomItemEvent = 1051; + public static final int BuildersClubPlaceWallItemEvent = 462; public static final int RequestInventoryPetsEvent = 3095; public static final int ModToolRequestRoomVisitsEvent = 3526; public static final int ModToolWarnEvent = -1;//3763 @@ -205,6 +208,7 @@ public class Incoming { public static final int RequestRoomDataEvent = 2230; public static final int RequestRoomHeightmapEvent = 2300; public static final int RequestGuildFurniWidgetEvent = 2651; + public static final int ClickFurniEvent = 6002; public static final int RequestOwnItemsEvent = 2105; public static final int RequestReportRoomEvent = 3267; public static final int ReportEvent = 1691; @@ -379,7 +383,7 @@ public class Incoming { public static final int UNKNOWN_SNOWSTORM_6000 = 6000; public static final int UNKNOWN_SNOWSTORM_6001 = 6001; - public static final int UNKNOWN_SNOWSTORM_6002 = 6002; + // public static final int UNKNOWN_SNOWSTORM_6002 = 6002; public static final int UNKNOWN_SNOWSTORM_6003 = 6003; public static final int UNKNOWN_SNOWSTORM_6004 = 6004; public static final int UNKNOWN_SNOWSTORM_6005 = 6005; @@ -407,6 +411,46 @@ public class Incoming { // CUSTOM public static final int UpdateFurniturePositionEvent = 10019; + public static final int ClickUserEvent = 10020; + public static final int WiredMonitorRequestEvent = 10021; + public static final int WiredRoomSettingsRequestEvent = 10022; + public static final int WiredRoomSettingsSaveEvent = 10023; + public static final int WiredUserVariablesRequestEvent = 10024; + public static final int WiredUserVariableUpdateEvent = 10025; + public static final int WiredUserVariableManageEvent = 10026; + public static final int WiredUserInspectMoveEvent = 10027; public static final int RequestInventoryPetDelete = 10030; public static final int RequestInventoryBadgeDelete = 10031; + + // Furni Editor + public static final int FurniEditorSearchEvent = 10040; + public static final int FurniEditorDetailEvent = 10041; + public static final int FurniEditorBySpriteEvent = 10042; + public static final int FurniEditorInteractionsEvent = 10043; + public static final int FurniEditorUpdateEvent = 10044; + public static final int FurniEditorDeleteEvent = 10045; + + // Catalog Admin + public static final int CatalogAdminSavePageEvent = 10050; + public static final int CatalogAdminCreatePageEvent = 10051; + public static final int CatalogAdminDeletePageEvent = 10052; + public static final int CatalogAdminSaveOfferEvent = 10053; + public static final int CatalogAdminCreateOfferEvent = 10054; + public static final int CatalogAdminDeleteOfferEvent = 10055; + public static final int CatalogAdminMoveOfferEvent = 10056; + public static final int CatalogAdminMovePageEvent = 10057; + public static final int CatalogAdminPublishEvent = 10058; + public static final int CatalogAdminSavePageImagesEvent = 10060; + public static final int CatalogAdminSavePageIconEvent = 10061; + + // Custom Prefixes + public static final int RequestUserPrefixesEvent = 7011; + public static final int SetActivePrefixEvent = 7012; + public static final int DeletePrefixEvent = 7013; + public static final int PurchasePrefixEvent = 7014; + + // YouTube Room Broadcast + public static final int YouTubeRoomPlayEvent = 8001; + public static final int YouTubeRoomWatchingEvent = 8002; + public static final int YouTubeRoomSettingsEvent = 8003; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceRoomItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceRoomItemEvent.java new file mode 100644 index 00000000..4684147b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceRoomItemEvent.java @@ -0,0 +1,160 @@ +package com.eu.habbo.messages.incoming.catalog; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogItem; +import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; +import com.eu.habbo.habbohotel.rooms.FurnitureMovementError; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; + +import java.util.Iterator; + +public class BuildersClubPlaceRoomItemEvent extends MessageHandler { + @Override + public void handle() throws Exception { + int pageId = this.packet.readInt(); + int offerId = this.packet.readInt(); + String extraData = this.packet.readString(); + short x = this.packet.readInt().shortValue(); + short y = this.packet.readInt().shortValue(); + int rotation = this.packet.readInt(); + + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + int placementUserId = BuildersClubRoomSupport.getPlacementPoolUserId(this.client.getHabbo()); + + if (room == null || !this.client.getHabbo().getRoomUnit().isInRoom()) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.NO_RIGHTS.errorCode)); + return; + } + + if (!BuildersClubRoomSupport.canPlaceInCurrentRoom(this.client.getHabbo())) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "builder.placement_widget.error.not_group_admin")); + BuildersClubRoomSupport.sendPlacementStatus(this.client.getHabbo()); + return; + } + + if (placementUserId <= 0) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.NO_RIGHTS.errorCode)); + return; + } + + if (!BuildersClubRoomSupport.hasActiveMembership(this.client.getHabbo().getHabboInfo().getId())) { + int trackedFurniCount = BuildersClubRoomSupport.getTrackedFurniCount(placementUserId); + + if (trackedFurniCount >= BuildersClubRoomSupport.getFurniLimit(placementUserId)) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "room.error.max_furniture")); + BuildersClubRoomSupport.sendPlacementStatus(this.client.getHabbo()); + return; + } + + if (BuildersClubRoomSupport.hasPlacementVisitors(room, this.client.getHabbo())) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "builder.placement_widget.error.visitors")); + return; + } + } + + CatalogItem catalogItem = resolveCatalogItem(pageId, offerId); + Item baseItem = resolveBaseItem(catalogItem, FurnitureType.FLOOR); + + if (catalogItem == null || baseItem == null) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode)); + return; + } + + RoomTile tile = room.getLayout().getTile(x, y); + + if (tile == null) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode)); + return; + } + + HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(placementUserId, baseItem, 0, 0, (extraData != null && !extraData.isEmpty()) ? extraData : catalogItem.getExtradata()); + + if (item == null) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode)); + return; + } + + item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID); + + FurnitureMovementError error = room.canPlaceFurnitureAt(item, this.client.getHabbo(), tile, rotation); + + if (!error.equals(FurnitureMovementError.NONE)) { + Emulator.getGameEnvironment().getItemManager().deleteItem(item); + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, error.errorCode)); + return; + } + + error = room.placeFloorFurniAt(item, tile, rotation, this.client.getHabbo()); + + if (!error.equals(FurnitureMovementError.NONE)) { + Emulator.getGameEnvironment().getItemManager().deleteItem(item); + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, error.errorCode)); + return; + } + + BuildersClubRoomSupport.trackPlacedItem(item.getId(), placementUserId, room.getId()); + + if (BuildersClubRoomSupport.syncRoom(room) == BuildersClubRoomSupport.SyncResult.LOCKED) { + BuildersClubRoomSupport.sendRoomLockedBubble(room.getOwnerId()); + } + + BuildersClubRoomSupport.sendPlacementStatusForPool(room, placementUserId); + } + + private CatalogItem resolveCatalogItem(int pageId, int offerId) { + CatalogItem buildersClubItem = Emulator.getGameEnvironment().getCatalogManager().getCatalogItem(offerId, CatalogPageType.BUILDER); + + if (buildersClubItem != null) { + return buildersClubItem; + } + + int catalogItemId = Emulator.getGameEnvironment().getCatalogManager().offerDefs.get(offerId); + + if (catalogItemId > 0) { + return Emulator.getGameEnvironment().getCatalogManager().getCatalogItem(catalogItemId); + } + + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, CatalogPageType.BUILDER); + + if (page == null) { + return null; + } + + for (CatalogItem catalogItem : page.getCatalogItems().valueCollection()) { + if (catalogItem.getOfferId() == offerId) { + return catalogItem; + } + } + + return null; + } + + private Item resolveBaseItem(CatalogItem catalogItem, FurnitureType expectedType) { + if (catalogItem == null || catalogItem.getAmount() != 1 || catalogItem.getBaseItems().size() != 1) { + return null; + } + + Iterator iterator = catalogItem.getBaseItems().iterator(); + + if (!iterator.hasNext()) { + return null; + } + + Item baseItem = iterator.next(); + + if (baseItem == null || baseItem.getType() != expectedType) { + return null; + } + + return baseItem; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceWallItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceWallItemEvent.java new file mode 100644 index 00000000..6c2822cf --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubPlaceWallItemEvent.java @@ -0,0 +1,142 @@ +package com.eu.habbo.messages.incoming.catalog; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogItem; +import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; +import com.eu.habbo.habbohotel.items.FurnitureType; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; +import com.eu.habbo.habbohotel.rooms.FurnitureMovementError; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; + +import java.util.Iterator; + +public class BuildersClubPlaceWallItemEvent extends MessageHandler { + @Override + public void handle() throws Exception { + int pageId = this.packet.readInt(); + int offerId = this.packet.readInt(); + String extraData = this.packet.readString(); + String wallPosition = this.packet.readString(); + + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + int placementUserId = BuildersClubRoomSupport.getPlacementPoolUserId(this.client.getHabbo()); + + if (room == null || !this.client.getHabbo().getRoomUnit().isInRoom()) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.NO_RIGHTS.errorCode)); + return; + } + + if (!BuildersClubRoomSupport.canPlaceInCurrentRoom(this.client.getHabbo())) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "builder.placement_widget.error.not_group_admin")); + BuildersClubRoomSupport.sendPlacementStatus(this.client.getHabbo()); + return; + } + + if (placementUserId <= 0) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.NO_RIGHTS.errorCode)); + return; + } + + if (!BuildersClubRoomSupport.hasActiveMembership(this.client.getHabbo().getHabboInfo().getId())) { + int trackedFurniCount = BuildersClubRoomSupport.getTrackedFurniCount(placementUserId); + + if (trackedFurniCount >= BuildersClubRoomSupport.getFurniLimit(placementUserId)) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "room.error.max_furniture")); + BuildersClubRoomSupport.sendPlacementStatus(this.client.getHabbo()); + return; + } + + if (BuildersClubRoomSupport.hasPlacementVisitors(room, this.client.getHabbo())) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "builder.placement_widget.error.visitors")); + return; + } + } + + CatalogItem catalogItem = resolveCatalogItem(pageId, offerId); + Item baseItem = resolveBaseItem(catalogItem, FurnitureType.WALL); + + if (catalogItem == null || baseItem == null) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode)); + return; + } + + HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(placementUserId, baseItem, 0, 0, (extraData != null && !extraData.isEmpty()) ? extraData : catalogItem.getExtradata()); + + if (item == null) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode)); + return; + } + + item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID); + + FurnitureMovementError error = room.placeWallFurniAt(item, wallPosition, this.client.getHabbo()); + + if (!error.equals(FurnitureMovementError.NONE)) { + Emulator.getGameEnvironment().getItemManager().deleteItem(item); + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, error.errorCode)); + return; + } + + BuildersClubRoomSupport.trackPlacedItem(item.getId(), placementUserId, room.getId()); + + if (BuildersClubRoomSupport.syncRoom(room) == BuildersClubRoomSupport.SyncResult.LOCKED) { + BuildersClubRoomSupport.sendRoomLockedBubble(room.getOwnerId()); + } + + BuildersClubRoomSupport.sendPlacementStatusForPool(room, placementUserId); + } + + private CatalogItem resolveCatalogItem(int pageId, int offerId) { + CatalogItem buildersClubItem = Emulator.getGameEnvironment().getCatalogManager().getCatalogItem(offerId, CatalogPageType.BUILDER); + + if (buildersClubItem != null) { + return buildersClubItem; + } + + int catalogItemId = Emulator.getGameEnvironment().getCatalogManager().offerDefs.get(offerId); + + if (catalogItemId > 0) { + return Emulator.getGameEnvironment().getCatalogManager().getCatalogItem(catalogItemId); + } + + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, CatalogPageType.BUILDER); + + if (page == null) { + return null; + } + + for (CatalogItem catalogItem : page.getCatalogItems().valueCollection()) { + if (catalogItem.getOfferId() == offerId) { + return catalogItem; + } + } + + return null; + } + + private Item resolveBaseItem(CatalogItem catalogItem, FurnitureType expectedType) { + if (catalogItem == null || catalogItem.getAmount() != 1 || catalogItem.getBaseItems().size() != 1) { + return null; + } + + Iterator iterator = catalogItem.getBaseItems().iterator(); + + if (!iterator.hasNext()) { + return null; + } + + Item baseItem = iterator.next(); + + if (baseItem == null || baseItem.getType() != expectedType) { + return null; + } + + return baseItem; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubQueryFurniCountEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubQueryFurniCountEvent.java new file mode 100644 index 00000000..8f733776 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/BuildersClubQueryFurniCountEvent.java @@ -0,0 +1,11 @@ +package com.eu.habbo.messages.incoming.catalog; + +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; +import com.eu.habbo.messages.incoming.MessageHandler; + +public class BuildersClubQueryFurniCountEvent extends MessageHandler { + @Override + public void handle() throws Exception { + BuildersClubRoomSupport.sendPlacementStatus(this.client.getHabbo()); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemAsGiftEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemAsGiftEvent.java index 98937b16..2416f7b1 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemAsGiftEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemAsGiftEvent.java @@ -44,15 +44,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { @Override public void handle() throws Exception { + LOGGER.error("DEBUG GIFT: entered CatalogBuyItemAsGiftEvent.handle()"); + if (Emulator.getIntUnixTimestamp() - this.client.getHabbo().getHabboStats().lastGiftTimestamp >= CatalogManager.PURCHASE_COOLDOWN) { this.client.getHabbo().getHabboStats().lastGiftTimestamp = Emulator.getIntUnixTimestamp(); + if (ShutdownEmulator.timestamp > 0) { + LOGGER.error("DEBUG GIFT: emulator closing"); this.client.sendResponse(new HotelWillCloseInMinutesComposer((ShutdownEmulator.timestamp - Emulator.getIntUnixTimestamp()) / 60)); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); return; } if (this.client.getHabbo().getHabboStats().isPurchasingFurniture) { + LOGGER.error("DEBUG GIFT: isPurchasingFurniture already true"); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); return; } else { @@ -60,7 +65,6 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { } try { - int pageId = this.packet.readInt(); int itemId = this.packet.readInt(); String extraData = this.packet.readString(); @@ -71,14 +75,22 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { int ribbonId = this.packet.readInt(); boolean showName = this.packet.readBoolean(); + LOGGER.error( + "DEBUG GIFT: pageId={}, itemId={}, extraData={}, username={}, spriteId={}, color={}, ribbonId={}, showName={}, message={}", + pageId, itemId, extraData, username, spriteId, color, ribbonId, showName, message + ); + int userId = 0; - if (!Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId) && !Emulator.getGameEnvironment().getCatalogManager().giftFurnis.containsKey(spriteId)) { + if (!Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId) + && !Emulator.getGameEnvironment().getCatalogManager().giftFurnis.containsKey(spriteId)) { + LOGGER.error("DEBUG GIFT: invalid spriteId for gift wrapper/furni -> {}", spriteId); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); return; } if (!GiftConfigurationComposer.BOX_TYPES.contains(color) || !GiftConfigurationComposer.RIBBON_TYPES.contains(ribbonId)) { + LOGGER.error("DEBUG GIFT: invalid color/ribbon -> color={}, ribbonId={}", color, ribbonId); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); return; } @@ -89,10 +101,12 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { Integer iItemId = Emulator.getGameEnvironment().getCatalogManager().giftWrappers.get(spriteId); - if (iItemId == null) + if (iItemId == null) { iItemId = Emulator.getGameEnvironment().getCatalogManager().giftFurnis.get(spriteId); + } if (iItemId == null) { + LOGGER.error("DEBUG GIFT: iItemId null for spriteId={}", spriteId); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); return; } @@ -100,9 +114,15 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { Item giftItem = Emulator.getGameEnvironment().getItemManager().getItem(iItemId); if (giftItem == null) { - giftItem = Emulator.getGameEnvironment().getItemManager().getItem((Integer) Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray()[Emulator.getRandom().nextInt(Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size())]); + LOGGER.error("DEBUG GIFT: direct giftItem null, trying random fallback. iItemId={}", iItemId); + giftItem = Emulator.getGameEnvironment().getItemManager().getItem( + (Integer) Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray()[ + Emulator.getRandom().nextInt(Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size()) + ] + ); if (giftItem == null) { + LOGGER.error("DEBUG GIFT: fallback giftItem also null"); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); return; } @@ -112,6 +132,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username); if (habbo == null) { + LOGGER.error("DEBUG GIFT: target user not online, checking DB -> {}", username); try (PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE username = ?")) { statement.setString(1, username); @@ -128,6 +149,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { } if (userId == 0) { + LOGGER.error("DEBUG GIFT: receiver not found -> {}", username); this.client.sendResponse(new GiftReceiverNotFoundComposer()); return; } @@ -135,11 +157,17 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId); if (page == null) { + LOGGER.error("DEBUG GIFT: page null -> {}", pageId); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); return; } if (page.getRank() > this.client.getHabbo().getHabboInfo().getRank().getId() || !page.isEnabled() || !page.isVisible()) { + LOGGER.error("DEBUG GIFT: page access denied. pageRank={}, userRank={}, enabled={}, visible={}", + page.getRank(), + this.client.getHabbo().getHabboInfo().getRank().getId(), + page.isEnabled(), + page.isVisible()); this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL)); return; } @@ -147,17 +175,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { CatalogItem item = page.getCatalogItem(itemId); if (item == null) { + LOGGER.error("DEBUG GIFT: catalog item null -> {}", itemId); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); return; } if (item.isClubOnly() && !this.client.getHabbo().getHabboStats().hasActiveClub()) { + LOGGER.error("DEBUG GIFT: item requires club -> itemId={}", itemId); this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.REQUIRES_CLUB)); return; } for (Item baseItem : item.getBaseItems()) { if (!baseItem.allowGift()) { + LOGGER.error("DEBUG GIFT: base item not giftable -> baseItemId={}, name={}", baseItem.getId(), baseItem.getName()); this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL)); return; } @@ -165,6 +196,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { if (item.isLimited()) { if (item.getLimitedStack() == item.getLimitedSells()) { + LOGGER.error("DEBUG GIFT: LTD sold out -> itemId={}", itemId); this.client.sendResponse(new AlertLimitedSoldOutComposer()); return; } @@ -173,7 +205,14 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { int totalCredits = item.getCredits(); int totalPoints = item.getPoints(); - if(totalCredits > this.client.getHabbo().getHabboInfo().getCredits() || totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType())) { + if (totalCredits > this.client.getHabbo().getHabboInfo().getCredits() + || totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType())) { + LOGGER.error("DEBUG GIFT: not enough currency. creditsNeeded={}, creditsHave={}, pointsNeeded={}, pointsHave={}, pointsType={}", + totalCredits, + this.client.getHabbo().getHabboInfo().getCredits(), + totalPoints, + this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType()), + item.getPointsType()); this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL)); return; } @@ -181,23 +220,34 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { CatalogLimitedConfiguration limitedConfiguration = null; int limitedStack = 0; int limitedNumber = 0; + if (item.isLimited()) { if (Emulator.getGameEnvironment().getCatalogManager().getLimitedConfig(item).available() == 0) { + LOGGER.error("DEBUG GIFT: LTD available=0 -> itemId={}", itemId); this.client.sendResponse(new AlertLimitedSoldOutComposer()); return; } - // Check daily LTD limits for the buyer (sender of the gift) if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) { int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total"); if (this.client.getHabbo().getHabboStats().totalLtds() >= ltdLimit) { - this.client.getHabbo().alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total").replace("%itemname%", item.getBaseItems().iterator().next().getFullName()).replace("%limit%", ltdLimit + "")); + LOGGER.error("DEBUG GIFT: sender reached daily total LTD limit"); + this.client.getHabbo().alert( + Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total") + .replace("%itemname%", item.getBaseItems().iterator().next().getFullName()) + .replace("%limit%", ltdLimit + "") + ); return; } ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item"); if (this.client.getHabbo().getHabboStats().totalLtds(item.getId()) >= ltdLimit) { - this.client.getHabbo().alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item").replace("%itemname%", item.getBaseItems().iterator().next().getFullName()).replace("%limit%", ltdLimit + "")); + LOGGER.error("DEBUG GIFT: sender reached daily LTD item limit"); + this.client.getHabbo().alert( + Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item") + .replace("%itemname%", item.getBaseItems().iterator().next().getFullName()) + .replace("%limit%", ltdLimit + "") + ); return; } } @@ -210,8 +260,6 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { limitedNumber = limitedConfiguration.getNumber(); limitedStack = limitedConfiguration.getTotalSet(); - - // Log the LTD purchase for daily limits this.client.getHabbo().getHabboStats().addLtdLog(item.getId(), Emulator.getIntUnixTimestamp()); } @@ -229,6 +277,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) as c FROM users_badges WHERE user_id = ? AND badge_code LIKE ?")) { statement.setInt(1, userId); statement.setString(2, baseItem.getName()); + try (ResultSet rSet = statement.executeQuery()) { if (rSet.next()) { c = rSet.getInt("c"); @@ -244,17 +293,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { } if (badgeFound) { + LOGGER.error("DEBUG GIFT: receiver already has badge"); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.ALREADY_HAVE_BADGE)); return; } if (item.getAmount() > 1 || item.getBaseItems().size() > 1) { + LOGGER.error("DEBUG GIFT: unsupported multi amount/baseItems. amount={}, baseItems={}", item.getAmount(), item.getBaseItems().size()); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); return; } for (Item baseItem : item.getBaseItems()) { if (item.getItemAmount(baseItem.getId()) > 1) { + LOGGER.error("DEBUG GIFT: unsupported item amount > 1 for baseItemId={}", baseItem.getId()); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); return; } @@ -278,37 +330,88 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { badgeFound = true; } } else if (item.getName().startsWith("rentable_bot_")) { + LOGGER.error("DEBUG GIFT: rentable bot gifts not supported"); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); return; } else if (Item.isPet(baseItem)) { + LOGGER.error("DEBUG GIFT: pet gifts not supported"); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); return; } else { - if (baseItem.getInteractionType().getType() == InteractionTrophy.class || baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class) { - if (baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class && habbo != null && !habbo.getClient().getHabbo().getInventory().getBadgesComponent().hasBadge(extraData)) { - ScripterManager.scripterDetected(habbo.getClient(), Emulator.getTexts().getValue("scripter.warning.catalog.badge_display").replace("%username%", habbo.getClient().getHabbo().getHabboInfo().getUsername()).replace("%badge%", extraData)); + if (baseItem.getInteractionType().getType() == InteractionTrophy.class + || baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class) { + if (baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class + && habbo != null + && !habbo.getClient().getHabbo().getInventory().getBadgesComponent().hasBadge(extraData)) { + ScripterManager.scripterDetected( + habbo.getClient(), + Emulator.getTexts().getValue("scripter.warning.catalog.badge_display") + .replace("%username%", habbo.getClient().getHabbo().getHabboInfo().getUsername()) + .replace("%badge%", extraData) + ); extraData = "UMAD"; } - extraData = this.client.getHabbo().getHabboInfo().getUsername() + (char) 9 + Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "-" + (Calendar.getInstance().get(Calendar.MONTH) + 1) + "-" + Calendar.getInstance().get(Calendar.YEAR) + (char) 9 + extraData; + extraData = this.client.getHabbo().getHabboInfo().getUsername() + + (char) 9 + + Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + + "-" + + (Calendar.getInstance().get(Calendar.MONTH) + 1) + + "-" + + Calendar.getInstance().get(Calendar.YEAR) + + (char) 9 + + extraData; } - if (baseItem.getInteractionType().getType() == InteractionTeleport.class || baseItem.getInteractionType().getType() == InteractionTeleportTile.class) { - HabboItem teleportOne = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData); - HabboItem teleportTwo = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData); + if (baseItem.getInteractionType().getType() == InteractionTeleport.class + || baseItem.getInteractionType().getType() == InteractionTeleportTile.class) { + + HabboItem teleportOne = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData); + HabboItem teleportTwo = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData); + + if (teleportOne == null || teleportTwo == null) { + LOGGER.error("DEBUG GIFT: teleport creation failed. baseItemId={}, teleportOneNull={}, teleportTwoNull={}", + baseItem.getId(), teleportOne == null, teleportTwo == null); + this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); + return; + } + Emulator.getGameEnvironment().getItemManager().insertTeleportPair(teleportOne.getId(), teleportTwo.getId()); itemsList.add(teleportOne); itemsList.add(teleportTwo); + } else if (baseItem.getInteractionType().getType() == InteractionHopper.class) { - HabboItem hopper = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedNumber, limitedNumber, extraData); + HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedNumber, limitedNumber, extraData); - Emulator.getGameEnvironment().getItemManager().insertHopper(hopper); + if (habboItem == null) { + LOGGER.error("DEBUG GIFT: hopper creation failed. baseItemId={}", baseItem.getId()); + this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); + return; + } - itemsList.add(hopper); - } else if (baseItem.getInteractionType().getType() == InteractionGuildFurni.class || baseItem.getInteractionType().getType() == InteractionGuildGate.class) { - InteractionGuildFurni habboItem = (InteractionGuildFurni) Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData); + Emulator.getGameEnvironment().getItemManager().insertHopper(habboItem); + itemsList.add(habboItem); + + } else if (baseItem.getInteractionType().getType() == InteractionGuildFurni.class + || baseItem.getInteractionType().getType() == InteractionGuildGate.class) { + HabboItem createdItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData); + + if (createdItem == null) { + LOGGER.error("DEBUG GIFT: guild item creation failed. baseItemId={}", baseItem.getId()); + this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); + return; + } + + if (!(createdItem instanceof InteractionGuildFurni)) { + LOGGER.error("DEBUG GIFT: created guild item has wrong class -> {}", createdItem.getClass().getName()); + this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); + return; + } + + InteractionGuildFurni habboItem = (InteractionGuildFurni) createdItem; habboItem.setExtradata(""); habboItem.needsUpdate(true); + int guildId; try { guildId = Integer.parseInt(extraData); @@ -317,15 +420,24 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); return; } + Emulator.getThreading().run(habboItem); Emulator.getGameEnvironment().getGuildManager().setGuild(habboItem, guildId); itemsList.add(habboItem); } else { - HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData); + HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData); + + if (habboItem == null) { + LOGGER.error("DEBUG GIFT: normal item creation failed. baseItemId={}, baseItemName={}", baseItem.getId(), baseItem.getName()); + this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); + return; + } + itemsList.add(habboItem); } } } else { + LOGGER.error("DEBUG GIFT: avatar_effect not supported"); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); this.client.sendResponse(new GenericAlertComposer(Emulator.getTexts().getValue("error.catalog.buy.not_yet"))); return; @@ -333,48 +445,85 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { } } - StringBuilder giftData = new StringBuilder(itemsList.size() + "\t"); - - for (HabboItem i : itemsList) { - giftData.append(i.getId()).append("\t"); - } - - giftData.append(color).append("\t").append(ribbonId).append("\t").append(showName ? "1" : "0").append("\t").append(message.replace("\t", "")).append("\t").append(this.client.getHabbo().getHabboInfo().getUsername()).append("\t").append(this.client.getHabbo().getHabboInfo().getLook()); - - HabboItem gift = Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, giftData.toString(), 0, 0); - - if (gift == null) { + if (itemsList.isEmpty()) { + LOGGER.error("DEBUG GIFT: itemsList empty before giftData"); + this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); + return; + } + + StringBuilder giftData = new StringBuilder(itemsList.size() + "\t"); + + for (HabboItem i : itemsList) { + if (i == null) { + LOGGER.error("DEBUG GIFT: null HabboItem detected inside itemsList"); + this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); + return; + } + + giftData.append(i.getId()).append("\t"); + } + + giftData.append(color) + .append("\t") + .append(ribbonId) + .append("\t") + .append(showName ? "1" : "0") + .append("\t") + .append(message.replace("\t", "")) + .append("\t") + .append(this.client.getHabbo().getHabboInfo().getUsername()) + .append("\t") + .append(this.client.getHabbo().getHabboInfo().getLook()); + + HabboItem gift = Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, giftData.toString(), 0, 0); + + if (gift == null) { + LOGGER.error("DEBUG GIFT: createGift returned null"); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); return; } - // Mark limited items as sold in the database to prevent duplication after catalog reload if (limitedConfiguration != null) { for (HabboItem itm : itemsList) { + if (itm == null) { + LOGGER.error("DEBUG GIFT: null item before limitedSold()"); + this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); + return; + } limitedConfiguration.limitedSold(item.getId(), this.client.getHabbo(), itm); } } if (this.client.getHabbo().getHabboInfo().getId() != userId) { - AchievementManager.progressAchievement(this.client.getHabbo(), Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftGiver")); + AchievementManager.progressAchievement( + this.client.getHabbo(), + Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftGiver") + ); } if (habbo != null) { habbo.getClient().sendResponse(new AddHabboItemComposer(gift)); habbo.getClient().getHabbo().getInventory().getItemsComponent().addItem(gift); habbo.getClient().sendResponse(new InventoryRefreshComposer()); + THashMap keys = new THashMap<>(); keys.put("display", "BUBBLE"); keys.put("image", "${image.library.url}notifications/gift.gif"); keys.put("message", Emulator.getTexts().getValue("generic.gift.received.anonymous")); + if (showName) { - keys.put("message", Emulator.getTexts().getValue("generic.gift.received").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername())); + keys.put("message", Emulator.getTexts().getValue("generic.gift.received") + .replace("%username%", this.client.getHabbo().getHabboInfo().getUsername())); } + habbo.getClient().sendResponse(new BubbleAlertComposer(BubbleAlertKeys.RECEIVED_BADGE.key, keys)); } if (this.client.getHabbo().getHabboInfo().getId() != userId) { - AchievementManager.progressAchievement(userId, Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftReceiver")); + AchievementManager.progressAchievement( + userId, + Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftReceiver") + ); } if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) { @@ -382,6 +531,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { this.client.getHabbo().giveCredits(-totalCredits); } } + if (totalPoints > 0) { if (item.getPointsType() == 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_PIXELS)) { this.client.getHabbo().givePixels(-totalPoints); @@ -390,16 +540,18 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler { } } + LOGGER.error("DEBUG GIFT: success sending PurchaseOKComposer"); this.client.sendResponse(new PurchaseOKComposer(item)); - } catch (Exception e) { - LOGGER.error("Exception caught", e); - this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); } + } catch (Exception e) { + LOGGER.error("Exception caught", e); + this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); } finally { this.client.getHabbo().getHabboStats().isPurchasingFurniture = false; } } else { + LOGGER.error("DEBUG GIFT: cooldown blocked purchase"); this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR)); } } -} +} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java index eb1086e3..e4aac1b0 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java @@ -7,6 +7,7 @@ import com.eu.habbo.habbohotel.catalog.layouts.*; import com.eu.habbo.habbohotel.items.FurnitureType; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.pets.PetManager; +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; import com.eu.habbo.habbohotel.rooms.RoomManager; import com.eu.habbo.habbohotel.users.HabboBadge; import com.eu.habbo.habbohotel.users.HabboInventory; @@ -14,6 +15,8 @@ import com.eu.habbo.habbohotel.users.subscriptions.Subscription; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.catalog.AlertPurchaseFailedComposer; import com.eu.habbo.messages.outgoing.catalog.AlertPurchaseUnavailableComposer; +import com.eu.habbo.messages.outgoing.catalog.BuildersClubFurniCountComposer; +import com.eu.habbo.messages.outgoing.catalog.BuildersClubSubscriptionStatusComposer; import com.eu.habbo.messages.outgoing.catalog.PurchaseOKComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; @@ -139,10 +142,10 @@ public class CatalogBuyItemEvent extends MessageHandler { return; } - if (page instanceof ClubBuyLayout || page instanceof VipBuyLayout) { + if (this.isClubOfferPage(page)) { ClubOffer item = Emulator.getGameEnvironment().getCatalogManager().clubOffers.get(itemId); - if (item == null) { + if (item == null || !item.belongsToWindow(this.getClubOfferWindowId(page))) { this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); return; } @@ -170,20 +173,19 @@ public class CatalogBuyItemEvent extends MessageHandler { if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_POINTS)) this.client.getHabbo().givePoints(item.getPointsType(), -totalDuckets); + if (item.isBuildersClubAddon()) { + this.client.getHabbo().getHabboStats().addBuildersClubBonusFurni(totalDays); + this.client.sendResponse(new BuildersClubFurniCountComposer(BuildersClubRoomSupport.getTrackedFurniCount(this.client.getHabbo().getHabboInfo().getId()))); + this.client.sendResponse(new BuildersClubSubscriptionStatusComposer(this.client.getHabbo())); + } else { + String subscriptionType = item.isBuildersClubSubscription() ? Subscription.BUILDERS_CLUB : Subscription.HABBO_CLUB; - if(this.client.getHabbo().getHabboStats().createSubscription(Subscription.HABBO_CLUB, (totalDays * 86400)) == null) { - this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); - throw new Exception("Unable to create or extend subscription"); + if (this.client.getHabbo().getHabboStats().createSubscription(subscriptionType, (totalDays * 86400)) == null) { + this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); + throw new Exception("Unable to create or extend subscription"); + } } - /*if (this.client.getHabbo().getHabboStats().getClubExpireTimestamp() <= Emulator.getIntUnixTimestamp()) - this.client.getHabbo().getHabboStats().setClubExpireTimestamp(Emulator.getIntUnixTimestamp()); - - this.client.getHabbo().getHabboStats().setClubExpireTimestamp(this.client.getHabbo().getHabboStats().getClubExpireTimestamp() + (totalDays * 86400)); - - this.client.sendResponse(new UserPermissionsComposer(this.client.getHabbo())); - this.client.sendResponse(new UserClubComposer(this.client.getHabbo()));*/ - this.client.sendResponse(new PurchaseOKComposer(null)); this.client.sendResponse(new InventoryRefreshComposer()); @@ -223,4 +225,24 @@ public class CatalogBuyItemEvent extends MessageHandler { this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose()); } } + + private boolean isClubOfferPage(CatalogPage page) { + return page instanceof ClubBuyLayout + || page instanceof VipBuyLayout + || page instanceof BuildersClubFrontPageLayout + || page instanceof BuildersClubAddonsLayout + || page instanceof BuildersClubLoyaltyLayout; + } + + private int getClubOfferWindowId(CatalogPage page) { + if (page instanceof BuildersClubAddonsLayout) { + return ClubOffer.WINDOW_BUILDERS_CLUB_ADDONS; + } + + if (page instanceof BuildersClubFrontPageLayout || page instanceof BuildersClubLoyaltyLayout) { + return ClubOffer.WINDOW_BUILDERS_CLUB; + } + + return ClubOffer.WINDOW_HABBO_CLUB; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/PurchaseTargetOfferEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/PurchaseTargetOfferEvent.java index 89fbfb3c..13fc307f 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/PurchaseTargetOfferEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/PurchaseTargetOfferEvent.java @@ -9,6 +9,11 @@ import com.eu.habbo.messages.incoming.MessageHandler; public class PurchaseTargetOfferEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 1000; + } + @Override public void handle() throws Exception { final int offerId = this.packet.readInt(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogModeEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogModeEvent.java index 9e54083a..c40c81f3 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogModeEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogModeEvent.java @@ -1,20 +1,17 @@ package com.eu.habbo.messages.incoming.catalog; import com.eu.habbo.messages.incoming.MessageHandler; -import com.eu.habbo.messages.outgoing.catalog.CatalogModeComposer; +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; import com.eu.habbo.messages.outgoing.catalog.CatalogPagesListComposer; public class RequestCatalogModeEvent extends MessageHandler { @Override public void handle() throws Exception { - String MODE = this.packet.readString(); - if (MODE.equalsIgnoreCase("normal")) { - this.client.sendResponse(new CatalogModeComposer(0)); - this.client.sendResponse(new CatalogPagesListComposer(this.client.getHabbo(), MODE)); - } else { - this.client.sendResponse(new CatalogModeComposer(1)); - this.client.sendResponse(new CatalogPagesListComposer(this.client.getHabbo(), MODE)); + this.client.sendResponse(new CatalogPagesListComposer(this.client.getHabbo(), MODE)); + + if (!MODE.equalsIgnoreCase("normal")) { + BuildersClubRoomSupport.sendPlacementStatus(this.client.getHabbo()); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogPageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogPageEvent.java index 0198c01e..bf2ba215 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogPageEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/RequestCatalogPageEvent.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.catalog; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.modtool.ScripterManager; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.catalog.CatalogPageComposer; @@ -13,8 +14,9 @@ public class RequestCatalogPageEvent extends MessageHandler { int catalogPageId = this.packet.readInt(); int offerId = this.packet.readInt(); String mode = this.packet.readString(); + CatalogPageType requestedType = CatalogPageType.fromString(mode); - CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(catalogPageId); + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(catalogPageId, requestedType); if (catalogPageId > 0 && page != null) { if (page.getRank() <= this.client.getHabbo().getHabboInfo().getRank().getId() && page.isEnabled()) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreateOfferEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreateOfferEvent.java new file mode 100644 index 00000000..380a3e07 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreateOfferEvent.java @@ -0,0 +1,81 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; + +public class CatalogAdminCreateOfferEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission")); + return; + } + + int pageId = this.packet.readInt(); + String itemIds = this.packet.readString(); + String catalogName = this.packet.readString(); + int costCredits = this.packet.readInt(); + int costPoints = this.packet.readInt(); + int pointsType = this.packet.readInt(); + int amount = this.packet.readInt(); + int clubOnly = this.packet.readInt(); + String extradata = this.packet.readString(); + boolean haveOffer = this.packet.readBoolean(); + int offerIdGroup = this.packet.readInt(); + int limitedStack = this.packet.readInt(); + int orderNumber = this.packet.readInt(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); + + int newId = -1; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + (pageType == CatalogPageType.BUILDER) + ? "INSERT INTO catalog_items_bc (page_id, item_ids, catalog_name, order_number, extradata) VALUES (?, ?, ?, ?, ?)" + : "INSERT INTO catalog_items (page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, club_only, extradata, have_offer, offer_id, limited_stack, order_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + Statement.RETURN_GENERATED_KEYS)) { + String cleanItemIds = (itemIds == null || itemIds.trim().isEmpty()) ? "0" : itemIds.trim(); + statement.setInt(1, pageId); + statement.setString(2, cleanItemIds); + statement.setString(3, catalogName); + + if (pageType == CatalogPageType.BUILDER) { + statement.setInt(4, orderNumber); + statement.setString(5, extradata); + } else { + statement.setInt(4, costCredits); + statement.setInt(5, costPoints); + statement.setInt(6, pointsType); + statement.setInt(7, amount); + statement.setString(8, clubOnly == 1 ? "1" : "0"); + statement.setString(9, extradata); + statement.setString(10, haveOffer ? "1" : "0"); + statement.setInt(11, offerIdGroup); + statement.setInt(12, limitedStack); + statement.setInt(13, orderNumber); + } + statement.execute(); + + try (ResultSet keys = statement.getGeneratedKeys()) { + if (keys.next()) { + newId = keys.getInt(1); + } + } + } + + if (newId > 0) { + this.client.sendResponse(new CatalogAdminResultComposer(true, "Offer created: " + newId)); + } else { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Failed to create offer")); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreatePageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreatePageEvent.java new file mode 100644 index 00000000..598582ad --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreatePageEvent.java @@ -0,0 +1,50 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.catalog.CatalogPageLayouts; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; + +public class CatalogAdminCreatePageEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission")); + return; + } + + String caption = this.packet.readString(); + String caption2 = this.packet.readString(); + String layout = this.packet.readString(); + int iconType = this.packet.readInt(); + int minRank = this.packet.readInt(); + boolean visible = this.packet.readBoolean(); + boolean enabled = this.packet.readBoolean(); + int orderNum = this.packet.readInt(); + int parentId = this.packet.readInt(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); + CatalogPageType catalogMode = CatalogPageType.fromString(this.packet.readString()); + + CatalogPageLayouts pageLayout; + try { + pageLayout = CatalogPageLayouts.valueOf(layout); + } catch (IllegalArgumentException e) { + pageLayout = CatalogPageLayouts.default_3x3; + } + + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().createCatalogPage( + caption, caption2, 0, iconType, pageLayout, minRank, parentId, pageType, catalogMode + ); + + if (page == null) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Failed to create page")); + return; + } + + this.client.sendResponse(new CatalogAdminResultComposer(true, "Page created: " + page.getId())); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeleteOfferEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeleteOfferEvent.java new file mode 100644 index 00000000..1363af8c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeleteOfferEvent.java @@ -0,0 +1,32 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +public class CatalogAdminDeleteOfferEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission")); + return; + } + + int offerId = this.packet.readInt(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement((pageType == CatalogPageType.BUILDER) ? "DELETE FROM catalog_items_bc WHERE id = ?" : "DELETE FROM catalog_items WHERE id = ?")) { + statement.setInt(1, offerId); + statement.execute(); + } + + this.client.sendResponse(new CatalogAdminResultComposer(true, "Offer deleted")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeletePageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeletePageEvent.java new file mode 100644 index 00000000..c72f0273 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeletePageEvent.java @@ -0,0 +1,46 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +public class CatalogAdminDeletePageEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission")); + return; + } + + int pageId = this.packet.readInt(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); + + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType); + + if (page == null) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId)); + return; + } + + String query = (pageType == CatalogPageType.BUILDER) + ? "DELETE FROM catalog_pages_bc WHERE id = ?" + : "DELETE FROM catalog_pages WHERE id = ?"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + statement.setInt(1, pageId); + statement.execute(); + } + + Emulator.getGameEnvironment().getCatalogManager().getCatalogPagesMap(pageType).remove(pageId); + + this.client.sendResponse(new CatalogAdminResultComposer(true, "Page deleted")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMoveOfferEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMoveOfferEvent.java new file mode 100644 index 00000000..f6be0130 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMoveOfferEvent.java @@ -0,0 +1,34 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +public class CatalogAdminMoveOfferEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission")); + return; + } + + int offerId = this.packet.readInt(); + int orderNumber = this.packet.readInt(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement((pageType == CatalogPageType.BUILDER) ? "UPDATE catalog_items_bc SET order_number = ? WHERE id = ?" : "UPDATE catalog_items SET order_number = ? WHERE id = ?")) { + statement.setInt(1, orderNumber); + statement.setInt(2, offerId); + statement.execute(); + } + + this.client.sendResponse(new CatalogAdminResultComposer(true, "Offer reordered")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMovePageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMovePageEvent.java new file mode 100644 index 00000000..20be1400 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMovePageEvent.java @@ -0,0 +1,64 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +public class CatalogAdminMovePageEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission")); + return; + } + + int pageId = this.packet.readInt(); + int newParentId = this.packet.readInt(); + int newIndex = this.packet.readInt(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); + String tableName = (pageType == CatalogPageType.BUILDER) ? "catalog_pages_bc" : "catalog_pages"; + + // Special values: -1 = toggle enabled, -2 = toggle visible + if (newParentId == -1) { + // Toggle enabled + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "UPDATE " + tableName + " SET enabled = IF(enabled = '1', '0', '1') WHERE id = ?")) { + statement.setInt(1, pageId); + statement.execute(); + } + this.client.sendResponse(new CatalogAdminResultComposer(true, "Page toggled")); + return; + } + + if (newParentId == -2) { + // Toggle visible + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "UPDATE " + tableName + " SET visible = IF(visible = '1', '0', '1') WHERE id = ?")) { + statement.setInt(1, pageId); + statement.execute(); + } + this.client.sendResponse(new CatalogAdminResultComposer(true, "Visibility toggled")); + return; + } + + // Normal move: update parent and order + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "UPDATE " + tableName + " SET parent_id = ?, order_num = ? WHERE id = ?")) { + statement.setInt(1, newParentId); + statement.setInt(2, newIndex); + statement.setInt(3, pageId); + statement.execute(); + } + + this.client.sendResponse(new CatalogAdminResultComposer(true, "Page moved")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminPublishEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminPublishEvent.java new file mode 100644 index 00000000..66a66cf5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminPublishEvent.java @@ -0,0 +1,26 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.CatalogUpdatedComposer; +import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; + +public class CatalogAdminPublishEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission")); + return; + } + + // Reload the entire catalog from database + Emulator.getGameEnvironment().getCatalogManager().initialize(); + + // Notify all connected clients that the catalog has been updated + Emulator.getGameServer().getGameClientManager().sendBroadcastResponse(new CatalogUpdatedComposer()); + + this.client.sendResponse(new CatalogAdminResultComposer(true, "Catalog published")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSaveOfferEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSaveOfferEvent.java new file mode 100644 index 00000000..1a5aff1a --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSaveOfferEvent.java @@ -0,0 +1,81 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +public class CatalogAdminSaveOfferEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission")); + return; + } + + int offerId = this.packet.readInt(); + int pageId = this.packet.readInt(); + String itemIds = this.packet.readString(); + String catalogName = this.packet.readString(); + int costCredits = this.packet.readInt(); + int costPoints = this.packet.readInt(); + int pointsType = this.packet.readInt(); + int amount = this.packet.readInt(); + int clubOnly = this.packet.readInt(); + String extradata = this.packet.readString(); + boolean haveOffer = this.packet.readBoolean(); + int offerIdGroup = this.packet.readInt(); + int limitedStack = this.packet.readInt(); + int orderNumber = this.packet.readInt(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); + + boolean updateItemIds = itemIds != null && !itemIds.trim().isEmpty(); + + String sql; + if (pageType == CatalogPageType.BUILDER) { + sql = updateItemIds + ? "UPDATE catalog_items_bc SET page_id = ?, item_ids = ?, catalog_name = ?, order_number = ?, extradata = ? WHERE id = ?" + : "UPDATE catalog_items_bc SET page_id = ?, catalog_name = ?, order_number = ?, extradata = ? WHERE id = ?"; + } else { + sql = updateItemIds + ? "UPDATE catalog_items SET page_id = ?, item_ids = ?, catalog_name = ?, cost_credits = ?, cost_points = ?, points_type = ?, amount = ?, club_only = ?, extradata = ?, have_offer = ?, offer_id = ?, limited_stack = ?, order_number = ? WHERE id = ?" + : "UPDATE catalog_items SET page_id = ?, catalog_name = ?, cost_credits = ?, cost_points = ?, points_type = ?, amount = ?, club_only = ?, extradata = ?, have_offer = ?, offer_id = ?, limited_stack = ?, order_number = ? WHERE id = ?"; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement(sql)) { + int idx = 1; + statement.setInt(idx++, pageId); + if (updateItemIds) { + statement.setString(idx++, itemIds.trim()); + } + statement.setString(idx++, catalogName); + + if (pageType == CatalogPageType.BUILDER) { + statement.setInt(idx++, orderNumber); + statement.setString(idx++, extradata); + statement.setInt(idx, offerId); + } else { + statement.setInt(idx++, costCredits); + statement.setInt(idx++, costPoints); + statement.setInt(idx++, pointsType); + statement.setInt(idx++, amount); + statement.setString(idx++, clubOnly == 1 ? "1" : "0"); + statement.setString(idx++, extradata); + statement.setString(idx++, haveOffer ? "1" : "0"); + statement.setInt(idx++, offerIdGroup); + statement.setInt(idx++, limitedStack); + statement.setInt(idx++, orderNumber); + statement.setInt(idx, offerId); + } + statement.execute(); + } + + this.client.sendResponse(new CatalogAdminResultComposer(true, "Offer saved")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageEvent.java new file mode 100644 index 00000000..7c8ab7ed --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageEvent.java @@ -0,0 +1,85 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +public class CatalogAdminSavePageEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission")); + return; + } + + int pageId = this.packet.readInt(); + String caption = this.packet.readString(); + String caption2 = this.packet.readString(); + String layout = this.packet.readString(); + int iconType = this.packet.readInt(); + int minRank = this.packet.readInt(); + boolean visible = this.packet.readBoolean(); + boolean enabled = this.packet.readBoolean(); + int orderNum = this.packet.readInt(); + int parentId = this.packet.readInt(); + String headline = this.packet.readString(); + String teaser = this.packet.readString(); + String textDetails = this.packet.readString(); + CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); + CatalogPageType catalogMode = CatalogPageType.fromString(this.packet.readString()); + + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType); + + if (page == null) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId)); + return; + } + + String query = (pageType == CatalogPageType.BUILDER) + ? "UPDATE catalog_pages_bc SET caption = ?, page_layout = ?, icon_image = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ? WHERE id = ?" + : "UPDATE catalog_pages SET caption = ?, caption_save = ?, page_layout = ?, icon_image = ?, min_rank = ?, visible = ?, enabled = ?, order_num = ?, parent_id = ?, page_headline = ?, page_teaser = ?, page_text_details = ?, catalog_mode = ? WHERE id = ?"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + statement.setString(1, caption); + + if (pageType == CatalogPageType.BUILDER) { + statement.setString(2, layout); + statement.setInt(3, iconType); + statement.setString(4, visible ? "1" : "0"); + statement.setString(5, enabled ? "1" : "0"); + statement.setInt(6, orderNum); + statement.setInt(7, parentId); + statement.setString(8, headline); + statement.setString(9, teaser); + statement.setString(10, textDetails); + statement.setInt(11, pageId); + } else { + statement.setString(2, caption2); + statement.setString(3, layout); + statement.setInt(4, iconType); + statement.setInt(5, minRank); + statement.setString(6, visible ? "1" : "0"); + statement.setString(7, enabled ? "1" : "0"); + statement.setInt(8, orderNum); + statement.setInt(9, parentId); + statement.setString(10, headline); + statement.setString(11, teaser); + statement.setString(12, textDetails); + statement.setString(13, catalogMode.name()); + statement.setInt(14, pageId); + } + + statement.execute(); + } + + this.client.sendResponse(new CatalogAdminResultComposer(true, "Page saved")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageIconEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageIconEvent.java new file mode 100644 index 00000000..8dd75010 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageIconEvent.java @@ -0,0 +1,41 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +public class CatalogAdminSavePageIconEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission")); + return; + } + + int pageId = this.packet.readInt(); + int iconId = this.packet.readInt(); + + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId); + + if (page == null) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId)); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "UPDATE catalog_pages SET icon_image = ? WHERE id = ?")) { + statement.setInt(1, iconId); + statement.setInt(2, pageId); + statement.execute(); + } + + this.client.sendResponse(new CatalogAdminResultComposer(true, "Page icon saved")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageImagesEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageImagesEvent.java new file mode 100644 index 00000000..29d2a8c2 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageImagesEvent.java @@ -0,0 +1,43 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +public class CatalogAdminSavePageImagesEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission")); + return; + } + + int pageId = this.packet.readInt(); + String headerImage = this.packet.readString(); + String teaserImage = this.packet.readString(); + + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId); + + if (page == null) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId)); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "UPDATE catalog_pages SET page_headline = ?, page_teaser = ? WHERE id = ?")) { + statement.setString(1, headerImage); + statement.setString(2, teaserImage); + statement.setInt(3, pageId); + statement.execute(); + } + + this.client.sendResponse(new CatalogAdminResultComposer(true, "Page images saved")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/BuyItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/BuyItemEvent.java index b9af0be8..5d604fb4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/BuyItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/BuyItemEvent.java @@ -4,6 +4,11 @@ import com.eu.habbo.habbohotel.catalog.marketplace.MarketPlace; import com.eu.habbo.messages.incoming.MessageHandler; public class BuyItemEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 1000; + } + @Override public void handle() throws Exception { int offerId = this.packet.readInt(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestCreditsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestCreditsEvent.java index b03fda10..af98004a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestCreditsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestCreditsEvent.java @@ -4,6 +4,11 @@ import com.eu.habbo.habbohotel.catalog.marketplace.MarketPlace; import com.eu.habbo.messages.incoming.MessageHandler; public class RequestCreditsEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 2000; + } + @Override public void handle() throws Exception { MarketPlace.getCredits(this.client); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/TakeBackItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/TakeBackItemEvent.java index 7586afc4..12e7b1ef 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/TakeBackItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/TakeBackItemEvent.java @@ -4,6 +4,11 @@ import com.eu.habbo.habbohotel.catalog.marketplace.MarketPlace; import com.eu.habbo.messages.incoming.MessageHandler; public class TakeBackItemEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 500; + } + @Override public void handle() throws Exception { int offerId = this.packet.readInt(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/AcceptFriendRequestEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/AcceptFriendRequestEvent.java index 869fe532..6017b08d 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/AcceptFriendRequestEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/AcceptFriendRequestEvent.java @@ -3,7 +3,6 @@ package com.eu.habbo.messages.incoming.friends; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.messenger.Messenger; import com.eu.habbo.habbohotel.users.Habbo; -import com.eu.habbo.habbohotel.users.HabboInfo; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.friends.FriendRequestErrorComposer; import org.slf4j.Logger; @@ -14,7 +13,6 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import static com.eu.habbo.habbohotel.users.HabboManager.getOfflineHabboInfo; public class AcceptFriendRequestEvent extends MessageHandler { private static final Logger LOGGER = LoggerFactory.getLogger(AcceptFriendRequestEvent.class); @@ -44,18 +42,10 @@ public class AcceptFriendRequestEvent extends MessageHandler { Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); if(target == null) { - HabboInfo habboInfo = getOfflineHabboInfo(userId); - - if(habboInfo == null) { - this.client.sendResponse(new FriendRequestErrorComposer(FriendRequestErrorComposer.TARGET_NOT_FOUND)); - this.client.getHabbo().getMessenger().deleteFriendRequests(userId, this.client.getHabbo().getHabboInfo().getId()); - continue; - } - - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.*, users_settings.block_friendrequests FROM users INNER JOIN users_settings ON users.id = users_settings.user_id WHERE username = ? LIMIT 1")) { - statement.setString(1, habboInfo.getUsername()); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.*, users_settings.block_friendrequests FROM users INNER JOIN users_settings ON users.id = users_settings.user_id WHERE users.id = ? LIMIT 1")) { + statement.setInt(1, userId); try (ResultSet set = statement.executeQuery()) { - while (set.next()) { + if (set.next()) { target = new Habbo(set); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/SearchUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/SearchUserEvent.java index d4b01757..9578fa69 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/SearchUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/SearchUserEvent.java @@ -9,7 +9,20 @@ import gnu.trove.set.hash.THashSet; import java.util.concurrent.ConcurrentHashMap; public class SearchUserEvent extends MessageHandler { - public static ConcurrentHashMap> cachedResults = new ConcurrentHashMap<>(); + private static final long CACHE_TTL_MS = 30_000; // 30 second TTL + private static final ConcurrentHashMap cacheTimestamps = new ConcurrentHashMap<>(); + public static final ConcurrentHashMap> cachedResults = new ConcurrentHashMap<>(); + + public static void cleanExpiredCache() { + long now = System.currentTimeMillis(); + cacheTimestamps.entrySet().removeIf(entry -> { + if (now - entry.getValue() > CACHE_TTL_MS) { + cachedResults.remove(entry.getKey()); + return true; + } + return false; + }); + } @Override public void handle() throws Exception { @@ -31,6 +44,7 @@ public class SearchUserEvent extends MessageHandler { if (buddies == null) { buddies = Messenger.searchUsers(username); cachedResults.put(username, buddies); + cacheTimestamps.put(username, System.currentTimeMillis()); } this.client.sendResponse(new UserSearchResultComposer(buddies, this.client.getHabbo().getMessenger().getFriends(username), this.client.getHabbo())); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniDataManager.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniDataManager.java new file mode 100644 index 00000000..8035bb9b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniDataManager.java @@ -0,0 +1,119 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Manages reading and writing of FurnitureData.json entries. + * Resolves the file path from emulator config keys. + */ +public class FurniDataManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(FurniDataManager.class); + + /** + * Get the JSON string for a specific item from FurnitureData.json. + * Returns "{}" if not found or on error. + */ + public static String getItemJson(int itemId) { + try { + Path furniDataPath = resolveFurniDataPath(); + if (furniDataPath == null || !Files.exists(furniDataPath)) { + return "{}"; + } + + String content = Files.readString(furniDataPath, StandardCharsets.UTF_8); + JsonObject root = JsonParser.parseString(content).getAsJsonObject(); + + // Search in both "roomitemtypes" and "wallitemtypes" + for (String section : new String[]{"roomitemtypes", "wallitemtypes"}) { + if (!root.has(section)) continue; + JsonObject sectionObj = root.getAsJsonObject(section); + if (!sectionObj.has("furnitype")) continue; + JsonArray types = sectionObj.getAsJsonArray("furnitype"); + + for (JsonElement el : types) { + JsonObject obj = el.getAsJsonObject(); + if (obj.has("id") && obj.get("id").getAsInt() == itemId) { + return obj.toString(); + } + } + } + } catch (Exception e) { + LOGGER.warn("Failed to read FurnitureData.json for item " + itemId, e); + } + + return "{}"; + } + + /** + * Resolve the path to FurnitureData.json from emulator config. + */ + private static Path resolveFurniDataPath() { + try { + String configPath = Emulator.getConfig().getValue("furni.editor.renderer.config.path", ""); + + if (configPath.isEmpty()) { + // Fallback: try common locations + String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", ""); + if (!basePath.isEmpty()) { + Path candidate = Paths.get(basePath, "FurnitureData.json"); + if (Files.exists(candidate)) return candidate; + } + return null; + } + + // Read the renderer config to find the furnidata URL/path + Path rendererConfig = Paths.get(configPath); + if (!Files.exists(rendererConfig)) return null; + + String rendererContent = Files.readString(rendererConfig, StandardCharsets.UTF_8); + JsonObject rendererObj = JsonParser.parseString(rendererContent).getAsJsonObject(); + + if (rendererObj.has("furnidata.url")) { + String furniUrl = rendererObj.get("furnidata.url").getAsString(); + + // Skip unresolved placeholders like ${gamedata.url} + if (furniUrl.contains("${")) { + String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", ""); + if (!basePath.isEmpty()) { + Path candidate = Paths.get(basePath, "FurnitureData.json"); + if (Files.exists(candidate)) return candidate; + } + return null; + } + + // Strip query string (?v=1 etc.) + String cleanUrl = furniUrl.contains("?") ? furniUrl.substring(0, furniUrl.indexOf('?')) : furniUrl; + + // If it's a local file path (not http), use it directly + if (!cleanUrl.startsWith("http")) { + return Paths.get(cleanUrl); + } + + // For http URLs, try to derive local path from base path + String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", ""); + if (!basePath.isEmpty()) { + // Extract filename from URL (without query string) + String filename = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1); + return Paths.get(basePath, filename); + } + } + } catch (Exception e) { + LOGGER.warn("Failed to resolve FurnitureData.json path", e); + } + + return null; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorBySpriteEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorBySpriteEvent.java new file mode 100644 index 00000000..e7f45362 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorBySpriteEvent.java @@ -0,0 +1,49 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +public class FurniEditorBySpriteEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No permission")); + return; + } + + int spriteId = this.packet.readInt(); + + if (spriteId <= 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid sprite ID")); + return; + } + + // Look up the item ID by sprite_id + int itemId = -1; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement stmt = connection.prepareStatement("SELECT id FROM items_base WHERE sprite_id = ? LIMIT 1")) { + stmt.setInt(1, spriteId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + itemId = rs.getInt("id"); + } + } + } + + if (itemId <= 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No item found with sprite_id: " + spriteId)); + return; + } + + // Delegate to the detail response builder + FurniEditorDetailEvent.sendDetailResponse(this.client, itemId); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDeleteEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDeleteEvent.java new file mode 100644 index 00000000..b31194dd --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDeleteEvent.java @@ -0,0 +1,87 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +public class FurniEditorDeleteEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No permission")); + return; + } + + int id = this.packet.readInt(); + + if (id <= 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid item ID")); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + // Check if item exists + try (PreparedStatement stmt = connection.prepareStatement("SELECT id FROM items_base WHERE id = ?")) { + stmt.setInt(1, id); + try (ResultSet rs = stmt.executeQuery()) { + if (!rs.next()) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Item not found: " + id)); + return; + } + } + } + + // Check usage count - items placed in rooms + int usageCount = 0; + try (PreparedStatement stmt = connection.prepareStatement("SELECT COUNT(*) FROM items WHERE item_id = ?")) { + stmt.setInt(1, id); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + usageCount = rs.getInt(1); + } + } + } + + if (usageCount > 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, + "Cannot delete: " + usageCount + " instances exist in the game")); + return; + } + + // Check catalog_items references + int catalogCount = 0; + try (PreparedStatement stmt = connection.prepareStatement( + "SELECT COUNT(*) FROM catalog_items WHERE item_ids LIKE ?")) { + stmt.setString(1, "%" + id + "%"); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + catalogCount = rs.getInt(1); + } + } + } + + if (catalogCount > 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, + "Cannot delete: item is referenced by " + catalogCount + " catalog entries")); + return; + } + + // Safe to delete + try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM items_base WHERE id = ?")) { + stmt.setInt(1, id); + stmt.executeUpdate(); + } + } + + // Reload emulator item definitions + Emulator.getGameEnvironment().getItemManager().loadItems(); + + this.client.sendResponse(new FurniEditorResultComposer(true, "Item deleted")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDetailEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDetailEvent.java new file mode 100644 index 00000000..b904fb55 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDetailEvent.java @@ -0,0 +1,96 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorDetailComposer; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class FurniEditorDetailEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No permission")); + return; + } + + int id = this.packet.readInt(); + + if (id <= 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid item ID")); + return; + } + + sendDetailResponse(this.client, id); + } + + /** + * Shared method to build and send a detail response for a given item ID. + * Used by both FurniEditorDetailEvent and FurniEditorBySpriteEvent. + */ + public static void sendDetailResponse(com.eu.habbo.habbohotel.gameclients.GameClient client, int itemId) throws Exception { + Map item = null; + int usageCount = 0; + List> catalogItems = new ArrayList<>(); + String furniDataJson = "{}"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + // Load full item data + try (PreparedStatement stmt = connection.prepareStatement("SELECT * FROM items_base WHERE id = ?")) { + stmt.setInt(1, itemId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + item = FurniEditorHelper.readFullItem(rs); + } + } + } + + if (item == null) { + client.sendResponse(new FurniEditorResultComposer(false, "Item not found: " + itemId)); + return; + } + + // Count placed instances + try (PreparedStatement stmt = connection.prepareStatement("SELECT COUNT(*) FROM items WHERE item_id = ?")) { + stmt.setInt(1, itemId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + usageCount = rs.getInt(1); + } + } + } + + // Load catalog references (join catalog_items with catalog_pages) + try (PreparedStatement stmt = connection.prepareStatement( + "SELECT ci.id AS ci_id, ci.catalog_name, ci.cost_credits, ci.cost_points, ci.points_type, " + + "ci.page_id AS ci_page_id, COALESCE(cp.caption, '') AS page_caption " + + "FROM catalog_items ci " + + "LEFT JOIN catalog_pages cp ON ci.page_id = cp.id " + + "WHERE ci.item_ids LIKE ?")) { + stmt.setString(1, "%" + itemId + "%"); + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + catalogItems.add(FurniEditorHelper.readCatalogRef(rs)); + } + } + } + } + + // Try to read furnidata.json entry + try { + furniDataJson = FurniDataManager.getItemJson(itemId); + } catch (Exception e) { + furniDataJson = "{}"; + } + + client.sendResponse(new FurniEditorDetailComposer(item, usageCount, catalogItems, furniDataJson)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorHelper.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorHelper.java new file mode 100644 index 00000000..0de0e1b6 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorHelper.java @@ -0,0 +1,123 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +/** + * Shared utility for building item data maps from ResultSet rows. + * Used by FurniEditorDetailEvent, FurniEditorBySpriteEvent, and + * FurniEditorSearchEvent to ensure consistent field reading. + */ +public class FurniEditorHelper { + + /** + * Read the 14 base fields from items_base into a Map. + */ + public static Map readBaseItem(ResultSet set) throws SQLException { + Map item = new HashMap<>(); + item.put("id", set.getInt("id")); + item.put("sprite_id", set.getInt("sprite_id")); + item.put("item_name", set.getString("item_name")); + item.put("public_name", set.getString("public_name")); + item.put("type", set.getString("type")); + item.put("width", set.getInt("width")); + item.put("length", set.getInt("length")); + item.put("stack_height", set.getDouble("stack_height")); + item.put("allow_stack", set.getString("allow_stack")); + item.put("allow_walk", set.getString("allow_walk")); + item.put("allow_sit", set.getString("allow_sit")); + item.put("allow_lay", set.getString("allow_lay")); + item.put("interaction_type", set.getString("interaction_type")); + item.put("interaction_modes_count", set.getInt("interaction_modes_count")); + return item; + } + + /** + * Read all fields (14 base + 13 extended) from items_base into a Map. + */ + public static Map readFullItem(ResultSet set) throws SQLException { + Map item = readBaseItem(set); + item.put("allow_gift", set.getString("allow_gift")); + item.put("allow_trade", set.getString("allow_trade")); + item.put("allow_recycle", set.getString("allow_recycle")); + item.put("allow_marketplace_sell", set.getString("allow_marketplace_sell")); + item.put("allow_inventory_stack", set.getString("allow_inventory_stack")); + item.put("vending_ids", set.getString("vending_ids")); + item.put("customparams", set.getString("customparams")); + item.put("effect_id_male", set.getInt("effect_id_male")); + item.put("effect_id_female", set.getInt("effect_id_female")); + item.put("clothing_on_walk", set.getString("clothing_on_walk")); + item.put("multiheight", set.getString("multiheight")); + + // description may not exist in all schemas, handle gracefully + try { + item.put("description", set.getString("description")); + } catch (SQLException e) { + item.put("description", ""); + } + + return item; + } + + /** + * Read a catalog item reference from a result set that joined + * catalog_items with catalog_pages. + */ + public static Map readCatalogRef(ResultSet set) throws SQLException { + Map ref = new HashMap<>(); + ref.put("id", set.getInt("ci_id")); + ref.put("catalog_name", set.getString("catalog_name")); + ref.put("cost_credits", set.getInt("cost_credits")); + ref.put("cost_points", set.getInt("cost_points")); + ref.put("points_type", set.getInt("points_type")); + ref.put("page_id", set.getInt("ci_page_id")); + ref.put("page_caption", set.getString("page_caption")); + return ref; + } + + /** + * Whitelist of allowed field names for update operations. + * Prevents SQL injection via arbitrary column names. + */ + public static final java.util.Set ALLOWED_UPDATE_FIELDS = java.util.Set.of( + "item_name", "public_name", "sprite_id", "type", "width", "length", + "stack_height", "allow_stack", "allow_walk", "allow_sit", "allow_lay", + "allow_gift", "allow_trade", "allow_recycle", "allow_marketplace_sell", + "allow_inventory_stack", "interaction_type", "interaction_modes_count", + "vending_ids", "customparams", "effect_id_male", "effect_id_female", + "clothing_on_walk", "multiheight", "description" + ); + + /** + * Map camelCase JS field names to DB column names. + */ + public static final Map FIELD_MAP = Map.ofEntries( + Map.entry("itemName", "item_name"), + Map.entry("publicName", "public_name"), + Map.entry("spriteId", "sprite_id"), + Map.entry("type", "type"), + Map.entry("width", "width"), + Map.entry("length", "length"), + Map.entry("stackHeight", "stack_height"), + Map.entry("allowStack", "allow_stack"), + Map.entry("allowWalk", "allow_walk"), + Map.entry("allowSit", "allow_sit"), + Map.entry("allowLay", "allow_lay"), + Map.entry("allowGift", "allow_gift"), + Map.entry("allowTrade", "allow_trade"), + Map.entry("allowRecycle", "allow_recycle"), + Map.entry("allowMarketplaceSell", "allow_marketplace_sell"), + Map.entry("allowInventoryStack", "allow_inventory_stack"), + Map.entry("interactionType", "interaction_type"), + Map.entry("interactionModesCount", "interaction_modes_count"), + Map.entry("vendingIds", "vending_ids"), + Map.entry("customparams", "customparams"), + Map.entry("effectIdMale", "effect_id_male"), + Map.entry("effectIdFemale", "effect_id_female"), + Map.entry("clothingOnWalk", "clothing_on_walk"), + Map.entry("multiheight", "multiheight"), + Map.entry("description", "description") + ); +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorInteractionsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorInteractionsEvent.java new file mode 100644 index 00000000..78dbb0d9 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorInteractionsEvent.java @@ -0,0 +1,45 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorInteractionsComposer; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class FurniEditorInteractionsEvent extends MessageHandler { + + private static List cachedInteractions = null; + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No permission")); + return; + } + + if (cachedInteractions == null) { + synchronized (FurniEditorInteractionsEvent.class) { + if (cachedInteractions == null) { + List list = new ArrayList<>(); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + Statement stmt = connection.createStatement(); + ResultSet set = stmt.executeQuery("SELECT DISTINCT interaction_type FROM items_base WHERE interaction_type != '' ORDER BY interaction_type ASC")) { + while (set.next()) { + list.add(set.getString("interaction_type")); + } + } + cachedInteractions = Collections.unmodifiableList(list); + } + } + } + + this.client.sendResponse(new FurniEditorInteractionsComposer(cachedInteractions)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorSearchEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorSearchEvent.java new file mode 100644 index 00000000..bfdc229c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorSearchEvent.java @@ -0,0 +1,115 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorSearchComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class FurniEditorSearchEvent extends MessageHandler { + + private static final int PAGE_SIZE = 20; + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No permission")); + return; + } + + String query = this.packet.readString(); + String type = this.packet.readString(); + int page = this.packet.readInt(); + + // Input validation + if (query.length() > 100) { + query = query.substring(0, 100); + } + + if (page < 1) page = 1; + + int offset = (page - 1) * PAGE_SIZE; + + // Build WHERE clause + StringBuilder whereClause = new StringBuilder("WHERE 1=1"); + List params = new ArrayList<>(); + + if (!query.isEmpty()) { + // Try numeric match first (id or sprite_id) + boolean isNumeric = false; + try { + int numericQuery = Integer.parseInt(query); + isNumeric = true; + whereClause.append(" AND (id = ? OR sprite_id = ? OR item_name LIKE ? OR public_name LIKE ?)"); + params.add(numericQuery); + params.add(numericQuery); + params.add("%" + query + "%"); + params.add("%" + query + "%"); + } catch (NumberFormatException e) { + whereClause.append(" AND (item_name LIKE ? OR public_name LIKE ?)"); + params.add("%" + query + "%"); + params.add("%" + query + "%"); + } + } + + if (type != null && !type.isEmpty()) { + whereClause.append(" AND type = ?"); + params.add(type); + } + + // Count total + int total = 0; + String countSql = "SELECT COUNT(*) FROM items_base " + whereClause; + String dataSql = "SELECT * FROM items_base " + whereClause + " ORDER BY id ASC LIMIT ? OFFSET ?"; + + List> items = new ArrayList<>(); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + // Get total count + try (PreparedStatement stmt = connection.prepareStatement(countSql)) { + int idx = 1; + for (Object param : params) { + if (param instanceof Integer) { + stmt.setInt(idx++, (Integer) param); + } else { + stmt.setString(idx++, (String) param); + } + } + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + total = rs.getInt(1); + } + } + } + + // Get items page + try (PreparedStatement stmt = connection.prepareStatement(dataSql)) { + int idx = 1; + for (Object param : params) { + if (param instanceof Integer) { + stmt.setInt(idx++, (Integer) param); + } else { + stmt.setString(idx++, (String) param); + } + } + stmt.setInt(idx++, PAGE_SIZE); + stmt.setInt(idx, offset); + + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + items.add(FurniEditorHelper.readBaseItem(rs)); + } + } + } + } + + this.client.sendResponse(new FurniEditorSearchComposer(items, total, page)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdateEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdateEvent.java new file mode 100644 index 00000000..845395f5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdateEvent.java @@ -0,0 +1,110 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class FurniEditorUpdateEvent extends MessageHandler { + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No permission")); + return; + } + + int id = this.packet.readInt(); + String jsonFieldsStr = this.packet.readString(); + + if (id <= 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid item ID")); + return; + } + + JsonObject json; + try { + json = JsonParser.parseString(jsonFieldsStr).getAsJsonObject(); + } catch (Exception e) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid JSON data")); + return; + } + + if (json.size() == 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No fields to update")); + return; + } + + // Build dynamic UPDATE with whitelisted fields + StringBuilder setClauses = new StringBuilder(); + List values = new ArrayList<>(); + + for (Map.Entry entry : json.entrySet()) { + String jsKey = entry.getKey(); + String dbColumn = FurniEditorHelper.FIELD_MAP.get(jsKey); + + if (dbColumn == null || !FurniEditorHelper.ALLOWED_UPDATE_FIELDS.contains(dbColumn)) { + continue; // Skip unknown or disallowed fields + } + + if (setClauses.length() > 0) setClauses.append(", "); + setClauses.append("`").append(dbColumn).append("` = ?"); + + JsonElement val = entry.getValue(); + if (val.isJsonPrimitive()) { + if (val.getAsJsonPrimitive().isBoolean()) { + values.add(val.getAsBoolean() ? "1" : "0"); + } else if (val.getAsJsonPrimitive().isNumber()) { + // Check if it's a decimal number + String numStr = val.getAsString(); + if (numStr.contains(".")) { + values.add(val.getAsDouble()); + } else { + values.add(val.getAsInt()); + } + } else { + values.add(val.getAsString()); + } + } else { + values.add(val.toString()); + } + } + + if (setClauses.length() == 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "No valid fields to update")); + return; + } + + String sql = "UPDATE items_base SET " + setClauses + " WHERE id = ?"; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement stmt = connection.prepareStatement(sql)) { + int idx = 1; + for (Object value : values) { + if (value instanceof Integer) { + stmt.setInt(idx++, (Integer) value); + } else if (value instanceof Double) { + stmt.setDouble(idx++, (Double) value); + } else { + stmt.setString(idx++, String.valueOf(value)); + } + } + stmt.setInt(idx, id); + stmt.executeUpdate(); + } + + // Reload emulator item definitions + Emulator.getGameEnvironment().getItemManager().loadItems(); + + this.client.sendResponse(new FurniEditorResultComposer(true, "Item updated", id)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeSettingsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeSettingsEvent.java index bf99d98c..5ad35f21 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeSettingsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeSettingsEvent.java @@ -2,16 +2,34 @@ package com.eu.habbo.messages.incoming.guilds; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.guilds.Guild; +import com.eu.habbo.habbohotel.guilds.GuildMember; import com.eu.habbo.habbohotel.guilds.GuildState; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.habbohotel.guilds.forums.ForumThread; +import com.eu.habbo.messages.incoming.guilds.forums.GuildForumListEvent; +import com.eu.habbo.messages.outgoing.guilds.GuildInfoComposer; +import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer; import com.eu.habbo.plugin.events.guilds.GuildChangedSettingsEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.concurrent.ConcurrentHashMap; public class GuildChangeSettingsEvent extends MessageHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(GuildChangeSettingsEvent.class); + + // Cooldown for forum toggle per guild: guildId -> last toggle timestamp + private static final ConcurrentHashMap forumToggleCooldown = new ConcurrentHashMap<>(); + private static final long FORUM_TOGGLE_COOLDOWN_MS = 30_000; // 30 seconds + @Override public int getRatelimit() { - return 500; + return 2000; // 2 seconds between settings saves } @Override @@ -31,6 +49,34 @@ public class GuildChangeSettingsEvent extends MessageHandler { guild.setState(GuildState.valueOf(settingsEvent.state)); guild.setRights(settingsEvent.rights); + // Read forum toggle + boolean forumEnabled = this.packet.readBoolean(); + boolean wasForumEnabled = guild.hasForum(); + + if (forumEnabled != wasForumEnabled) { + // Enforce cooldown on forum toggle to prevent rapid enable/disable spam + Long lastToggle = forumToggleCooldown.get(guildId); + long now = System.currentTimeMillis(); + + if (lastToggle != null && (now - lastToggle) < FORUM_TOGGLE_COOLDOWN_MS) { + LOGGER.warn("Forum toggle cooldown for guild {} by user {}", guildId, this.client.getHabbo().getHabboInfo().getUsername()); + } else { + forumToggleCooldown.put(guildId, now); + guild.setForum(forumEnabled); + + if (!forumEnabled) { + // Delete all threads and comments for this guild + ForumThread.clearCacheForGuild(guildId); + deleteForumData(guildId); + } + + // Invalidate caches + GuildForumDataComposer.invalidateUnreadCache(guildId); + GuildForumListEvent.invalidateActiveForumsCache(); + GuildForumListEvent.invalidateMyForumsCache(this.client.getHabbo().getHabboInfo().getId()); + } + } + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(guild.getRoomId()); if(room != null) { room.refreshGuild(guild); @@ -39,7 +85,38 @@ public class GuildChangeSettingsEvent extends MessageHandler { guild.needsUpdate = true; Emulator.getThreading().run(guild); + + // Send updated group info back to client so hasForum flag refreshes immediately + GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo()); + this.client.sendResponse(new GuildInfoComposer(guild, this.client, false, member)); } } } + + private void deleteForumData(int guildId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + // Delete comments for all threads in this guild + try (PreparedStatement statement = connection.prepareStatement( + "DELETE FROM `guilds_forums_comments` WHERE `thread_id` IN (SELECT `id` FROM `guilds_forums_threads` WHERE `guild_id` = ?)")) { + statement.setInt(1, guildId); + statement.executeUpdate(); + } + + // Delete all threads for this guild + try (PreparedStatement statement = connection.prepareStatement( + "DELETE FROM `guilds_forums_threads` WHERE `guild_id` = ?")) { + statement.setInt(1, guildId); + statement.executeUpdate(); + } + + // Delete forum view records for this guild + try (PreparedStatement statement = connection.prepareStatement( + "DELETE FROM `guild_forum_views` WHERE `guild_id` = ?")) { + statement.setInt(1, guildId); + statement.executeUpdate(); + } + } catch (SQLException e) { + LOGGER.error("Failed to delete forum data for guild " + guildId, e); + } + } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildDeleteEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildDeleteEvent.java index a1ce8c48..80677246 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildDeleteEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildDeleteEvent.java @@ -7,7 +7,6 @@ import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.guilds.GuildFavoriteRoomUserUpdateComposer; -import com.eu.habbo.messages.outgoing.guilds.RemoveGuildFromRoomComposer; import com.eu.habbo.messages.outgoing.rooms.RoomDataComposer; import com.eu.habbo.plugin.events.guilds.GuildDeletedEvent; import gnu.trove.set.hash.THashSet; @@ -38,11 +37,15 @@ public class GuildDeleteEvent extends MessageHandler { Emulator.getGameEnvironment().getGuildManager().deleteGuild(guild); Emulator.getPluginManager().fireEvent(new GuildDeletedEvent(guild, this.client.getHabbo())); - Emulator.getGameEnvironment().getRoomManager().getRoom(guild.getRoomId()).sendComposer(new RemoveGuildFromRoomComposer(guildId).compose()); + com.eu.habbo.habbohotel.rooms.Room guildRoom = Emulator.getGameEnvironment().getRoomManager().getRoom(guild.getRoomId()); - if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { - if (guild.getRoomId() == this.client.getHabbo().getHabboInfo().getCurrentRoom().getId()) { - this.client.sendResponse(new RoomDataComposer(this.client.getHabbo().getHabboInfo().getCurrentRoom(), this.client.getHabbo(), false, false)); + if (guildRoom != null) { + for (Habbo habbo : guildRoom.getHabbos()) { + if (habbo.getClient() == null) { + continue; + } + + habbo.getClient().sendResponse(new RoomDataComposer(guildRoom, habbo, true, false)); } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java index a6eeeef0..0171da26 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java @@ -12,6 +12,7 @@ import com.eu.habbo.messages.outgoing.catalog.PurchaseOKComposer; import com.eu.habbo.messages.outgoing.guilds.GuildBoughtComposer; import com.eu.habbo.messages.outgoing.guilds.GuildEditFailComposer; import com.eu.habbo.messages.outgoing.guilds.GuildInfoComposer; +import com.eu.habbo.messages.outgoing.rooms.RoomDataComposer; import com.eu.habbo.plugin.events.guilds.GuildPurchasedEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -96,20 +97,29 @@ public class RequestGuildBuyEvent extends MessageHandler { r.removeAllRights(); r.setNeedsUpdate(true); + Emulator.getGameEnvironment().getGuildManager().addGuild(guild); + if (Emulator.getConfig().getBoolean("imager.internal.enabled")) { Emulator.getBadgeImager().generate(guild); } this.client.sendResponse(new PurchaseOKComposer()); this.client.sendResponse(new GuildBoughtComposer(guild)); - for (Habbo habbo : r.getHabbos()) { - habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, null)); - } r.refreshGuild(guild); - Emulator.getPluginManager().fireEvent(new GuildPurchasedEvent(guild, this.client.getHabbo())); + for (Habbo habbo : r.getHabbos()) { + if (habbo.getClient() == null) { + continue; + } - Emulator.getGameEnvironment().getGuildManager().addGuild(guild); + habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, null)); + + if (habbo.getHabboInfo().getId() != this.client.getHabbo().getHabboInfo().getId()) { + habbo.getClient().sendResponse(new RoomDataComposer(r, habbo, true, false)); + } + } + + Emulator.getPluginManager().fireEvent(new GuildPurchasedEvent(guild, this.client.getHabbo())); } } else { String message = Emulator.getTexts().getValue("scripter.warning.guild.buy.owner").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%roomname%", r.getName().replace("%owner%", r.getOwnerName())); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumListEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumListEvent.java index 83e7b484..aa76d1c8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumListEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumListEvent.java @@ -14,6 +14,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; public class GuildForumListEvent extends MessageHandler { @Override @@ -23,6 +24,26 @@ public class GuildForumListEvent extends MessageHandler { private static final Logger LOGGER = LoggerFactory.getLogger(GuildForumListEvent.class); + // Cache for active forums list (shared across all users) + private static volatile THashSet activeForumsCache = null; + private static volatile long activeForumsCachedAt = 0; + private static final long ACTIVE_FORUMS_TTL = 30 * 60 * 1000; // 30 minutes + + // Cache for user's forum list + private static final ConcurrentHashMap myForumsCache = new ConcurrentHashMap<>(); // userId -> {cachedAt} + private static final ConcurrentHashMap> myForumsData = new ConcurrentHashMap<>(); + private static final long MY_FORUMS_TTL = 10 * 60 * 1000; // 10 minutes + + public static void invalidateActiveForumsCache() { + activeForumsCache = null; + activeForumsCachedAt = 0; + } + + public static void invalidateMyForumsCache(int userId) { + myForumsCache.remove(userId); + myForumsData.remove(userId); + } + @Override public void handle() throws Exception { int mode = this.packet.readInt(); @@ -50,12 +71,18 @@ public class GuildForumListEvent extends MessageHandler { } private THashSet getActiveForums() { + long now = System.currentTimeMillis(); + + if (activeForumsCache != null && (now - activeForumsCachedAt) < ACTIVE_FORUMS_TTL) { + return activeForumsCache; + } + THashSet guilds = new THashSet(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT `guilds`.`id`, SUM(`guilds_forums_threads`.`posts_count`) AS `post_count` " + "FROM `guilds_forums_threads` " + "LEFT JOIN `guilds` ON `guilds`.`id` = `guilds_forums_threads`.`guild_id` " + - "WHERE `guilds`.`read_forum` = 'EVERYONE' AND `guilds_forums_threads`.`created_at` > ? " + + "WHERE `guilds`.`forum` = '1' AND `guilds_forums_threads`.`created_at` > ? " + "GROUP BY `guilds`.`id` " + "ORDER BY `post_count` DESC LIMIT 100")) { statement.setInt(1, Emulator.getIntUnixTimestamp() - 7 * 24 * 60 * 60); @@ -73,10 +100,21 @@ public class GuildForumListEvent extends MessageHandler { this.client.sendResponse(new ConnectionErrorComposer(500)); } + activeForumsCache = guilds; + activeForumsCachedAt = now; + return guilds; } private THashSet getMyForums(int userId) { + long now = System.currentTimeMillis(); + + long[] cached = myForumsCache.get(userId); + if (cached != null && (now - cached[0]) < MY_FORUMS_TTL) { + THashSet data = myForumsData.get(userId); + if (data != null) return data; + } + THashSet guilds = new THashSet(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT `guilds`.`id` FROM `guilds_members` " + @@ -97,6 +135,9 @@ public class GuildForumListEvent extends MessageHandler { this.client.sendResponse(new ConnectionErrorComposer(500)); } + myForumsCache.put(userId, new long[]{now}); + myForumsData.put(userId, guilds); + return guilds; } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumMarkAsReadEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumMarkAsReadEvent.java new file mode 100644 index 00000000..ace83232 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumMarkAsReadEvent.java @@ -0,0 +1,50 @@ +package com.eu.habbo.messages.incoming.guilds.forums; + +import com.eu.habbo.Emulator; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public class GuildForumMarkAsReadEvent extends MessageHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(GuildForumMarkAsReadEvent.class); + + @Override + public int getRatelimit() { + return 2000; + } + + @Override + public void handle() throws Exception { + int count = this.packet.readInt(); + int userId = this.client.getHabbo().getHabboInfo().getId(); + int timestamp = Emulator.getIntUnixTimestamp(); + + for (int i = 0; i < count; i++) { + int guildId = this.packet.readInt(); + this.packet.readInt(); // messageId (not used, we track by timestamp) + this.packet.readBoolean(); // isRead + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( + "INSERT INTO `guild_forum_views` (`user_id`, `guild_id`, `timestamp`) VALUES (?, ?, ?) " + + "ON DUPLICATE KEY UPDATE `timestamp` = ?" + )) { + statement.setInt(1, userId); + statement.setInt(2, guildId); + statement.setInt(3, timestamp); + statement.setInt(4, timestamp); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + + // Invalidate caches so next request gets fresh data + GuildForumDataComposer.invalidateLastSeenCache(userId, guildId); + GuildForumDataComposer.invalidateUnreadCache(guildId); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateMessageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateMessageEvent.java index bb4d87eb..51b9ec0b 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateMessageEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateMessageEvent.java @@ -18,7 +18,7 @@ import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer; public class GuildForumModerateMessageEvent extends MessageHandler { @Override public int getRatelimit() { - return 500; + return 2000; } @Override @@ -36,6 +36,11 @@ public class GuildForumModerateMessageEvent extends MessageHandler { return; } + if (thread.getGuildId() != guildId) { + this.client.sendResponse(new ConnectionErrorComposer(403)); + return; + } + ForumThreadComment comment = thread.getCommentById(messageId); if (comment == null) { this.client.sendResponse(new ConnectionErrorComposer(404)); @@ -45,19 +50,20 @@ public class GuildForumModerateMessageEvent extends MessageHandler { boolean hasStaffPermissions = this.client.getHabbo().hasPermission(Permission.ACC_MODTOOL_TICKET_Q); GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId()); - if (member == null) { + if (member == null && !hasStaffPermissions) { this.client.sendResponse(new ConnectionErrorComposer(401)); return; } - boolean isGuildAdministrator = (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || member.getRank().equals(GuildRank.ADMIN)); + boolean isGuildAdministrator = (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || (member != null && member.getRank().equals(GuildRank.ADMIN))); if (!isGuildAdministrator && !hasStaffPermissions) { this.client.sendResponse(new ConnectionErrorComposer(403)); return; } - if (state == ForumThreadState.HIDDEN_BY_GUILD_ADMIN.getStateId() && !hasStaffPermissions) { + // Restrict state 20 (staff hidden) to staff only + if (state == 20 && !hasStaffPermissions) { this.client.sendResponse(new ConnectionErrorComposer(403)); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateThreadEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateThreadEvent.java index 95a972fa..3fa8905b 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateThreadEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateThreadEvent.java @@ -10,15 +10,24 @@ import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; +import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer; import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumThreadMessagesComposer; import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumThreadsComposer; import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; public class GuildForumModerateThreadEvent extends MessageHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(GuildForumModerateThreadEvent.class); + @Override public int getRatelimit() { - return 500; + return 2000; } @Override @@ -26,8 +35,6 @@ public class GuildForumModerateThreadEvent extends MessageHandler { int guildId = packet.readInt(); int threadId = packet.readInt(); int state = packet.readInt(); - // STATE 20 - HIDDEN_BY_GUILD_ADMIN = HIDDEN BY GUILD ADMINS/ HOTEL MODERATORS - // STATE 1 = VISIBLE THREAD Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId); ForumThread thread = ForumThread.getById(threadId); @@ -37,6 +44,11 @@ public class GuildForumModerateThreadEvent extends MessageHandler { return; } + if (thread.getGuildId() != guildId) { + this.client.sendResponse(new ConnectionErrorComposer(403)); + return; + } + GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId()); boolean hasStaffPerms = this.client.getHabbo().hasPermission(Permission.ACC_MODTOOL_TICKET_Q); @@ -52,12 +64,22 @@ public class GuildForumModerateThreadEvent extends MessageHandler { return; } - thread.setState(ForumThreadState.fromValue(state)); // sets state as defined in the packet + // State 20 = permanent delete (thread + comments removed from DB) + if (state == 20) { + deleteThread(threadId); + ForumThread.clearCacheForGuild(guildId); + GuildForumDataComposer.invalidateUnreadCache(guildId); + + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FORUMS_THREAD_HIDDEN.key).compose()); + this.client.sendResponse(new GuildForumThreadsComposer(guild, 0)); + return; + } + + thread.setState(ForumThreadState.fromValue(state)); thread.run(); switch (state) { case 10: - case 20: this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FORUMS_THREAD_HIDDEN.key).compose()); break; case 1: @@ -68,4 +90,22 @@ public class GuildForumModerateThreadEvent extends MessageHandler { this.client.sendResponse(new GuildForumThreadMessagesComposer(thread)); this.client.sendResponse(new GuildForumThreadsComposer(guild, 0)); } + + private void deleteThread(int threadId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + try (PreparedStatement statement = connection.prepareStatement( + "DELETE FROM `guilds_forums_comments` WHERE `thread_id` = ?")) { + statement.setInt(1, threadId); + statement.executeUpdate(); + } + + try (PreparedStatement statement = connection.prepareStatement( + "DELETE FROM `guilds_forums_threads` WHERE `id` = ?")) { + statement.setInt(1, threadId); + statement.executeUpdate(); + } + } catch (SQLException e) { + LOGGER.error("Failed to delete thread " + threadId, e); + } + } } \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumPostThreadEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumPostThreadEvent.java index a1730c3b..ae021b5c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumPostThreadEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumPostThreadEvent.java @@ -9,6 +9,7 @@ import com.eu.habbo.habbohotel.guilds.forums.ForumThreadComment; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumAddCommentComposer; +import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer; import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumThreadMessagesComposer; import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer; @@ -17,7 +18,7 @@ public class GuildForumPostThreadEvent extends MessageHandler { @Override public int getRatelimit() { - return 1000; + return 2000; } @Override @@ -65,6 +66,7 @@ public class GuildForumPostThreadEvent extends MessageHandler { this.client.getHabbo().getHabboStats().forumPostsCount += 1; thread.setPostsCount(thread.getPostsCount() + 1); + GuildForumDataComposer.invalidateUnreadCache(guildId); this.client.sendResponse(new GuildForumThreadMessagesComposer(thread)); return; } @@ -74,6 +76,15 @@ public class GuildForumPostThreadEvent extends MessageHandler { return; } + if (thread.getGuildId() != guildId) { + this.client.sendResponse(new ConnectionErrorComposer(403)); + return; + } + + if (thread.isLocked() && !isStaff) { + this.client.sendResponse(new ConnectionErrorComposer(403)); + return; + } if (!((guild.canPostMessages().state == 0) || (guild.canPostMessages().state == 1 && member != null) @@ -91,6 +102,7 @@ public class GuildForumPostThreadEvent extends MessageHandler { thread.setUpdatedAt(Emulator.getIntUnixTimestamp()); this.client.getHabbo().getHabboStats().forumPostsCount += 1; thread.setPostsCount(thread.getPostsCount() + 1); + GuildForumDataComposer.invalidateUnreadCache(guildId); this.client.sendResponse(new GuildForumAddCommentComposer(comment)); } else { this.client.sendResponse(new ConnectionErrorComposer(500)); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadUpdateEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadUpdateEvent.java index 908ff2a0..741a818f 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadUpdateEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadUpdateEvent.java @@ -17,7 +17,7 @@ import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer; public class GuildForumThreadUpdateEvent extends MessageHandler { @Override public int getRatelimit() { - return 500; + return 2000; } @Override diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsEvent.java index 009a2cee..3672a62a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsEvent.java @@ -20,7 +20,7 @@ public class GuildForumThreadsEvent extends MessageHandler { Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId); - if (guild == null || !guild.hasForum()) { + if (guild == null) { this.client.sendResponse(new ConnectionErrorComposer(404)); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsMessagesEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsMessagesEvent.java index 3fd6a50c..3680677f 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsMessagesEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsMessagesEvent.java @@ -37,6 +37,13 @@ public class GuildForumThreadsMessagesEvent extends MessageHandler { this.client.sendResponse(new ConnectionErrorComposer(404)); return; } + + // Verify thread belongs to the requested guild + if (thread.getGuildId() != guildId) { + this.client.sendResponse(new ConnectionErrorComposer(403)); + return; + } + GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId()); boolean isGuildAdministrator = (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || (member != null && member.getRank().equals(GuildRank.ADMIN))); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumUpdateSettingsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumUpdateSettingsEvent.java index 91a46b09..11916a7b 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumUpdateSettingsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumUpdateSettingsEvent.java @@ -12,7 +12,7 @@ import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer; public class GuildForumUpdateSettingsEvent extends MessageHandler { @Override public int getRatelimit() { - return 500; + return 2000; } @Override diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java index 00b469ba..13d47c28 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java @@ -2,18 +2,25 @@ package com.eu.habbo.messages.incoming.handshake; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.messenger.Messenger; +import com.eu.habbo.habbohotel.gameclients.GameClient; +import com.eu.habbo.habbohotel.gameclients.SessionResumeManager; import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem; import com.eu.habbo.habbohotel.modtool.ModToolSanctions; import com.eu.habbo.habbohotel.navigation.NavigatorSavedSearch; import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomManager; +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboManager; import com.eu.habbo.habbohotel.users.clothingvalidation.ClothingValidationManager; +import com.eu.habbo.habbohotel.users.subscriptions.Subscription; import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionHabboClub; import com.eu.habbo.messages.NoAuthMessage; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.BuildersClubSubscriptionStatusComposer; +import com.eu.habbo.messages.outgoing.commands.AvailableCommandsComposer; import com.eu.habbo.messages.outgoing.gamecenter.GameCenterAccountInfoComposer; import com.eu.habbo.messages.outgoing.gamecenter.GameCenterGameListComposer; import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer; @@ -30,7 +37,6 @@ import com.eu.habbo.messages.outgoing.modtool.ModToolComposer; import com.eu.habbo.messages.outgoing.modtool.ModToolSanctionInfoComposer; import com.eu.habbo.messages.outgoing.mysterybox.MysteryBoxKeysComposer; import com.eu.habbo.messages.outgoing.navigator.NewNavigatorSavedSearchesComposer; -import com.eu.habbo.messages.outgoing.unknown.BuildersClubExpiredComposer; import com.eu.habbo.messages.outgoing.users.*; import com.eu.habbo.plugin.events.emulator.SSOAuthenticationEvent; import com.eu.habbo.plugin.events.users.UserLoginEvent; @@ -81,31 +87,94 @@ public class SecureLoginEvent extends MessageHandler { } if (this.client.getHabbo() == null) { - Habbo habbo = Emulator.getGameEnvironment().getHabboManager().loadHabbo(sso); + // Store SSO ticket on client for grace period tracking + this.client.setSsoTicket(sso); + + // Race condition fix: if the old WebSocket connection is still alive on the + // server when the client reconnects, the SSO ticket won't be in the DB yet + // (it was cleared on first login, and parkHabbo hasn't run because the old + // channel hasn't closed). Find the old client by SSO ticket and force-dispose + // it, which parks the habbo and restores the ticket to the DB. + GameClient existingClient = Emulator.getGameServer().getGameClientManager().findClientBySsoTicket(sso); + if (existingClient != null && existingClient != this.client) { + LOGGER.info("[SessionResume] Found existing client with same SSO ticket — disposing old connection to trigger parking"); + Emulator.getGameServer().getGameClientManager().disposeClient(existingClient); + } + + // First, look up the user ID to check for ghost sessions + int lookupUserId = 0; + try (java.sql.Connection conn = Emulator.getDatabase().getDataSource().getConnection(); + java.sql.PreparedStatement stmt = conn.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) { + stmt.setString(1, sso); + try (java.sql.ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + lookupUserId = rs.getInt("id"); + } + } + } catch (Exception e) { + LOGGER.error("Caught exception looking up user for session resume", e); + } + + // Check if this user has a ghost session (disconnected within grace period) + Habbo habbo = null; + boolean isSessionResume = false; + + if (lookupUserId > 0) { + habbo = SessionResumeManager.getInstance().resumeSession(lookupUserId); + } + if (habbo != null) { - try { - habbo.setClient(this.client); - this.client.setHabbo(habbo); - if(!this.client.getHabbo().connect()) { + // Session resume — reattach the existing Habbo to the new client + isSessionResume = true; + LOGGER.info("[SessionResume] Resuming session for {} (id={})", + habbo.getHabboInfo().getUsername(), habbo.getHabboInfo().getId()); + + habbo.setClient(this.client); + this.client.setHabbo(habbo); + this.client.setMachineId(habbo.getHabboInfo().getMachineID()); + + // Clear the SSO ticket now that session is resumed (prevent reuse) + if (!Emulator.debugging) { + try (java.sql.Connection conn = Emulator.getDatabase().getDataSource().getConnection(); + java.sql.PreparedStatement stmt = conn.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) { + stmt.setString(1, ""); + stmt.setInt(2, habbo.getHabboInfo().getId()); + stmt.execute(); + } catch (Exception e) { + LOGGER.error("Failed to clear SSO ticket after session resume", e); + } + } + } else { + // Normal login — load from database + habbo = Emulator.getGameEnvironment().getHabboManager().loadHabbo(sso); + } + + if (habbo != null) { + if (!isSessionResume) { + try { + habbo.setClient(this.client); + this.client.setHabbo(habbo); + if(!this.client.getHabbo().connect()) { + Emulator.getGameServer().getGameClientManager().disposeClient(this.client); + return; + } + + if (this.client.getHabbo().getHabboInfo() == null) { + Emulator.getGameServer().getGameClientManager().disposeClient(this.client); + return; + } + + if (this.client.getHabbo().getHabboInfo().getRank() == null) { + throw new NullPointerException(habbo.getHabboInfo().getUsername() + " has a NON EXISTING RANK!"); + } + + Emulator.getThreading().run(habbo); + Emulator.getGameEnvironment().getHabboManager().addHabbo(habbo); + } catch (Exception e) { + LOGGER.error("Caught exception", e); Emulator.getGameServer().getGameClientManager().disposeClient(this.client); return; } - - if (this.client.getHabbo().getHabboInfo() == null) { - Emulator.getGameServer().getGameClientManager().disposeClient(this.client); - return; - } - - if (this.client.getHabbo().getHabboInfo().getRank() == null) { - throw new NullPointerException(habbo.getHabboInfo().getUsername() + " has a NON EXISTING RANK!"); - } - - Emulator.getThreading().run(habbo); - Emulator.getGameEnvironment().getHabboManager().addHabbo(habbo); - } catch (Exception e) { - LOGGER.error("Caught exception", e); - Emulator.getGameServer().getGameClientManager().disposeClient(this.client); - return; } if(ClothingValidationManager.VALIDATE_ON_LOGIN) { @@ -121,7 +190,18 @@ public class SecureLoginEvent extends MessageHandler { int roomIdToEnter = 0; - if (!this.client.getHabbo().getHabboStats().nux || Emulator.getConfig().getBoolean("retro.style.homeroom") && this.client.getHabbo().getHabboInfo().getHomeRoom() != 0) + if (isSessionResume) { + // On session resume, DON'T set roomIdToEnter. The client keeps its + // existing room view alive and the habbo is already in the room on + // the server. Setting roomIdToEnter = 0 prevents UserHomeRoomComposer + // from triggering a full room re-entry on the client (which would + // tear down and rebuild the room view). + Room currentRoom = habbo.getHabboInfo().getCurrentRoom(); + if (currentRoom != null) { + LOGGER.info("[SessionResume] {} is still in room {} — client will resume in-place", + habbo.getHabboInfo().getUsername(), currentRoom.getId()); + } + } else if (!this.client.getHabbo().getHabboStats().nux || Emulator.getConfig().getBoolean("retro.style.homeroom") && this.client.getHabbo().getHabboInfo().getHomeRoom() != 0) roomIdToEnter = this.client.getHabbo().getHabboInfo().getHomeRoom(); else if (!this.client.getHabbo().getHabboStats().nux || Emulator.getConfig().getBoolean("retro.style.homeroom") && RoomManager.HOME_ROOM_ID > 0) roomIdToEnter = RoomManager.HOME_ROOM_ID; @@ -131,13 +211,18 @@ public class SecureLoginEvent extends MessageHandler { messages.add(new UserClothesComposer(this.client.getHabbo()).compose()); messages.add(new NewUserIdentityComposer(habbo).compose()); messages.add(new UserPermissionsComposer(this.client.getHabbo()).compose()); + messages.add(new AvailableCommandsComposer( + Emulator.getGameEnvironment().getCommandHandler().getCommandsForRank( + this.client.getHabbo().getHabboInfo().getRank().getId() + ) + ).compose()); messages.add(new AvailabilityStatusMessageComposer(true, false, true).compose()); messages.add(new PingComposer().compose()); messages.add(new EnableNotificationsComposer(Emulator.getConfig().getBoolean("bubblealerts.enabled", true)).compose()); messages.add(new UserAchievementScoreComposer(this.client.getHabbo()).compose()); messages.add(new IsFirstLoginOfDayComposer(true).compose()); messages.add(new MysteryBoxKeysComposer().compose()); - messages.add(new BuildersClubExpiredComposer().compose()); + messages.add(new BuildersClubSubscriptionStatusComposer(this.client.getHabbo()).compose()); messages.add(new CfhTopicsMessageComposer().compose()); messages.add(new FavoriteRoomsCountComposer(this.client.getHabbo()).compose()); messages.add(new GameCenterGameListComposer().compose()); @@ -152,6 +237,33 @@ public class SecureLoginEvent extends MessageHandler { this.client.sendResponses(messages); + if (!isSessionResume) { + BuildersClubRoomSupport.syncOwnedRooms(this.client.getHabbo().getHabboInfo().getId()); + + boolean hasActiveBuildersClub = this.client.getHabbo().getHabboStats().hasSubscription(Subscription.BUILDERS_CLUB); + boolean hadBuildersClubBefore = false; + + for (com.eu.habbo.habbohotel.users.subscriptions.Subscription subscription : this.client.getHabbo().getHabboStats().subscriptions) { + if (subscription.getSubscriptionType().equalsIgnoreCase(Subscription.BUILDERS_CLUB)) { + hadBuildersClubBefore = true; + break; + } + } + + if (hasActiveBuildersClub) { + int remaining = BuildersClubRoomSupport.getMembershipSecondsLeft(this.client.getHabbo().getHabboInfo().getId()); + + if (remaining > 0 && remaining <= (72 * 3600)) { + BuildersClubRoomSupport.sendMembershipExpiringAlert(this.client.getHabbo().getHabboInfo().getId()); + } + } else if (hadBuildersClubBefore) { + BuildersClubRoomSupport.sendMembershipExpiredAlert( + this.client.getHabbo().getHabboInfo().getId(), + BuildersClubRoomSupport.hasTrackedItemsInOwnedRooms(this.client.getHabbo().getHabboInfo().getId()) + ); + } + } + //Hardcoded //this.client.sendResponse(new ForumsTestComposer()); this.client.sendResponse(new InventoryAchievementsComposer()); @@ -189,42 +301,45 @@ public class SecureLoginEvent extends MessageHandler { } } - UserLoginEvent userLoginEvent = new UserLoginEvent(habbo, this.client.getHabbo().getHabboInfo().getIpLogin()); - Emulator.getPluginManager().fireEvent(userLoginEvent); + // Skip login-only events on session resume (welcome alerts, login events, etc.) + if (!isSessionResume) { + UserLoginEvent userLoginEvent = new UserLoginEvent(habbo, this.client.getHabbo().getHabboInfo().getIpLogin()); + Emulator.getPluginManager().fireEvent(userLoginEvent); - if(userLoginEvent.isCancelled()) { - Emulator.getGameServer().getGameClientManager().disposeClient(this.client); - return; - } + if(userLoginEvent.isCancelled()) { + Emulator.getGameServer().getGameClientManager().disposeClient(this.client); + return; + } - if (Emulator.getConfig().getBoolean("hotel.welcome.alert.enabled")) { - final Habbo finalHabbo = habbo; - Emulator.getThreading().run(() -> { - if (Emulator.getConfig().getBoolean("hotel.welcome.alert.oldstyle")) { - SecureLoginEvent.this.client.sendResponse(new MessagesForYouComposer(HabboManager.WELCOME_MESSAGE.replace("%username%", finalHabbo.getHabboInfo().getUsername()).replace("%user%", finalHabbo.getHabboInfo().getUsername()).split("
"))); - } else { - SecureLoginEvent.this.client.sendResponse(new GenericAlertComposer(HabboManager.WELCOME_MESSAGE.replace("%username%", finalHabbo.getHabboInfo().getUsername()).replace("%user%", finalHabbo.getHabboInfo().getUsername()))); - } - }, Emulator.getConfig().getInt("hotel.welcome.alert.delay", 5000)); - } + if (Emulator.getConfig().getBoolean("hotel.welcome.alert.enabled")) { + final Habbo finalHabbo = habbo; + Emulator.getThreading().run(() -> { + if (Emulator.getConfig().getBoolean("hotel.welcome.alert.oldstyle")) { + SecureLoginEvent.this.client.sendResponse(new MessagesForYouComposer(HabboManager.WELCOME_MESSAGE.replace("%username%", finalHabbo.getHabboInfo().getUsername()).replace("%user%", finalHabbo.getHabboInfo().getUsername()).split("
"))); + } else { + SecureLoginEvent.this.client.sendResponse(new GenericAlertComposer(HabboManager.WELCOME_MESSAGE.replace("%username%", finalHabbo.getHabboInfo().getUsername()).replace("%user%", finalHabbo.getHabboInfo().getUsername()))); + } + }, Emulator.getConfig().getInt("hotel.welcome.alert.delay", 5000)); + } - if(SubscriptionHabboClub.HC_PAYDAY_ENABLED) { - SubscriptionHabboClub.processUnclaimed(habbo); - } + if(SubscriptionHabboClub.HC_PAYDAY_ENABLED) { + SubscriptionHabboClub.processUnclaimed(habbo); + } - SubscriptionHabboClub.processClubBadge(habbo); + SubscriptionHabboClub.processClubBadge(habbo); - Messenger.checkFriendSizeProgress(habbo); + Messenger.checkFriendSizeProgress(habbo); - if (!habbo.getHabboStats().hasGottenDefaultSavedSearches) { - habbo.getHabboStats().hasGottenDefaultSavedSearches = true; - Emulator.getThreading().run(habbo.getHabboStats()); + if (!habbo.getHabboStats().hasGottenDefaultSavedSearches) { + habbo.getHabboStats().hasGottenDefaultSavedSearches = true; + Emulator.getThreading().run(habbo.getHabboStats()); - habbo.getHabboInfo().addSavedSearch(new NavigatorSavedSearch("official-root", "")); - habbo.getHabboInfo().addSavedSearch(new NavigatorSavedSearch("my", "")); - habbo.getHabboInfo().addSavedSearch(new NavigatorSavedSearch("favorites", "")); + habbo.getHabboInfo().addSavedSearch(new NavigatorSavedSearch("official-root", "")); + habbo.getHabboInfo().addSavedSearch(new NavigatorSavedSearch("my", "")); + habbo.getHabboInfo().addSavedSearch(new NavigatorSavedSearch("favorites", "")); - this.client.sendResponse(new NewNavigatorSavedSearchesComposer(this.client.getHabbo().getHabboInfo().getSavedSearches())); + this.client.sendResponse(new NewNavigatorSavedSearchesComposer(this.client.getHabbo().getHabboInfo().getSavedSearches())); + } } } else { Emulator.getGameServer().getGameClientManager().disposeClient(this.client); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/hotelview/HotelViewRequestSecondsUntilEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/hotelview/HotelViewRequestSecondsUntilEvent.java index d948ffc8..9856b076 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/hotelview/HotelViewRequestSecondsUntilEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/hotelview/HotelViewRequestSecondsUntilEvent.java @@ -3,9 +3,9 @@ package com.eu.habbo.messages.incoming.hotelview; import com.eu.habbo.Emulator; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.hotelview.HotelViewSecondsUntilComposer; +import com.eu.habbo.util.HotelDateTimeUtil; import java.time.LocalDateTime; -import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; @@ -18,7 +18,7 @@ public class HotelViewRequestSecondsUntilEvent extends MessageHandler { try { LocalDateTime dt = LocalDateTime.parse(date, formatter); - int secondsUntil = Math.max(0, (int) dt.atZone(ZoneId.systemDefault()).toEpochSecond() - Emulator.getIntUnixTimestamp()); + int secondsUntil = Math.max(0, (int) HotelDateTimeUtil.toEpochSecond(dt) - Emulator.getIntUnixTimestamp()); this.client.sendResponse(new HotelViewSecondsUntilComposer(date, secondsUntil)); } catch (DateTimeParseException ignored) { } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/DeletePrefixEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/DeletePrefixEvent.java new file mode 100644 index 00000000..7cac1bf1 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/DeletePrefixEvent.java @@ -0,0 +1,23 @@ +package com.eu.habbo.messages.incoming.inventory.prefixes; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.UserPrefix; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.inventory.prefixes.UserPrefixesComposer; + +public class DeletePrefixEvent extends MessageHandler { + @Override + public void handle() throws Exception { + int prefixId = this.packet.readInt(); + + UserPrefix prefix = this.client.getHabbo().getInventory().getPrefixesComponent().getPrefix(prefixId); + + if (prefix == null) return; + + this.client.getHabbo().getInventory().getPrefixesComponent().removePrefix(prefix); + prefix.needsDelete(true); + Emulator.getThreading().run(prefix); + + this.client.sendResponse(new UserPrefixesComposer(this.client.getHabbo())); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchasePrefixEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchasePrefixEvent.java new file mode 100644 index 00000000..8e04000f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/PurchasePrefixEvent.java @@ -0,0 +1,145 @@ +package com.eu.habbo.messages.incoming.inventory.prefixes; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserPrefix; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; +import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; +import com.eu.habbo.messages.outgoing.inventory.prefixes.PrefixReceivedComposer; +import com.eu.habbo.messages.outgoing.users.UserCreditsComposer; +import com.eu.habbo.messages.outgoing.users.UserCurrencyComposer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class PurchasePrefixEvent extends MessageHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(PurchasePrefixEvent.class); + + @Override + public int getRatelimit() { + return 500; + } + + @Override + public void handle() throws Exception { + String text = this.packet.readString(); + String color = this.packet.readString(); + String icon = this.packet.readString(); + String effect = this.packet.readString(); + + Habbo habbo = this.client.getHabbo(); + + if (habbo == null) return; + + // Load settings + int maxLength = getSettingInt("max_length", 15); + int minRank = getSettingInt("min_rank_to_buy", 1); + int priceCredits = getSettingInt("price_credits", 5); + int pricePoints = getSettingInt("price_points", 0); + int pointsType = getSettingInt("points_type", 0); + + // Validate text + text = text.trim(); + + if (text.isEmpty() || text.length() > maxLength) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Prefix text is invalid or too long (max " + maxLength + " characters).")); + return; + } + + // Validate color (single hex or comma-separated multi hex for per-letter colors) + String[] colorParts = color.split(","); + for (String part : colorParts) { + if (!part.matches("^#[0-9A-Fa-f]{6}$")) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Invalid color format.")); + return; + } + } + + // Check rank + if (habbo.getHabboInfo().getRank().getId() < minRank) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Your rank is too low to purchase prefixes.")); + return; + } + + // Check blacklist + if (isBlacklisted(text)) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "This prefix contains a blocked word.")); + return; + } + + // Check credits + if (priceCredits > 0 && habbo.getHabboInfo().getCredits() < priceCredits) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough credits.")); + return; + } + + // Check points + if (pricePoints > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < pricePoints) { + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points.")); + return; + } + + // Deduct currency + if (priceCredits > 0) { + habbo.getHabboInfo().addCredits(-priceCredits); + this.client.sendResponse(new UserCreditsComposer(habbo)); + } + + if (pricePoints > 0) { + habbo.getHabboInfo().addCurrencyAmount(pointsType, -pricePoints); + this.client.sendResponse(new UserCurrencyComposer(habbo)); + } + + // Validate icon (allow empty or known icon names) + if (icon == null) icon = ""; + icon = icon.trim(); + + // Validate effect + if (effect == null) effect = ""; + effect = effect.trim(); + + // Create prefix + UserPrefix prefix = new UserPrefix(habbo.getHabboInfo().getId(), text, color, icon, effect); + prefix.run(); // Insert into DB synchronously to get the ID + habbo.getInventory().getPrefixesComponent().addPrefix(prefix); + + this.client.sendResponse(new PrefixReceivedComposer(prefix)); + } + + private int getSettingInt(String key, int defaultValue) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT `value` FROM custom_prefix_settings WHERE key_name = ?")) { + statement.setString(1, key); + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + return Integer.parseInt(set.getString("value")); + } + } + } catch (SQLException | NumberFormatException e) { + LOGGER.error("Error reading prefix setting: " + key, e); + } + return defaultValue; + } + + private boolean isBlacklisted(String text) { + String lowerText = text.toLowerCase(); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT word FROM custom_prefix_blacklist")) { + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + if (lowerText.contains(set.getString("word").toLowerCase())) { + return true; + } + } + } + } catch (SQLException e) { + LOGGER.error("Error checking prefix blacklist", e); + } + return false; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/RequestUserPrefixesEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/RequestUserPrefixesEvent.java new file mode 100644 index 00000000..b3169f70 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/RequestUserPrefixesEvent.java @@ -0,0 +1,11 @@ +package com.eu.habbo.messages.incoming.inventory.prefixes; + +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.inventory.prefixes.UserPrefixesComposer; + +public class RequestUserPrefixesEvent extends MessageHandler { + @Override + public void handle() throws Exception { + this.client.sendResponse(new UserPrefixesComposer(this.client.getHabbo())); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetActivePrefixEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetActivePrefixEvent.java new file mode 100644 index 00000000..9ec5710a --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/inventory/prefixes/SetActivePrefixEvent.java @@ -0,0 +1,25 @@ +package com.eu.habbo.messages.incoming.inventory.prefixes; + +import com.eu.habbo.habbohotel.users.UserPrefix; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.inventory.prefixes.ActivePrefixUpdatedComposer; + +public class SetActivePrefixEvent extends MessageHandler { + @Override + public void handle() throws Exception { + int prefixId = this.packet.readInt(); + + if (prefixId == 0) { + this.client.getHabbo().getInventory().getPrefixesComponent().deactivateAll(); + this.client.sendResponse(new ActivePrefixUpdatedComposer(null)); + return; + } + + UserPrefix prefix = this.client.getHabbo().getInventory().getPrefixesComponent().getPrefix(prefixId); + + if (prefix == null) return; + + this.client.getHabbo().getInventory().getPrefixesComponent().setActive(prefixId); + this.client.sendResponse(new ActivePrefixUpdatedComposer(prefix)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java index eb533cf9..48beb3c7 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java @@ -7,6 +7,11 @@ import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.incoming.MessageHandler; public class ModToolIssueDefaultSanctionEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 2000; + } + @Override public void handle() throws Exception { if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolKickEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolKickEvent.java index 3e1e234e..77370b36 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolKickEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolKickEvent.java @@ -4,6 +4,11 @@ import com.eu.habbo.Emulator; import com.eu.habbo.messages.incoming.MessageHandler; public class ModToolKickEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 2000; + } + @Override public void handle() throws Exception { Emulator.getGameEnvironment().getModToolManager().kick(this.client.getHabbo(), Emulator.getGameEnvironment().getHabboManager().getHabbo(this.packet.readInt()), this.packet.readString()); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java index 808a25de..4c901c46 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java @@ -12,6 +12,11 @@ import gnu.trove.map.hash.THashMap; import java.util.ArrayList; public class ModToolSanctionAlertEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 2000; + } + @Override public void handle() throws Exception { int userId = this.packet.readInt(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java index a451a2ad..1814041b 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java @@ -12,6 +12,11 @@ import gnu.trove.map.hash.THashMap; import java.util.ArrayList; public class ModToolSanctionBanEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 2000; + } + public static final int BAN_18_HOURS = 3; public static final int BAN_7_DAYS = 4; public static final int BAN_30_DAYS_STEP_1 = 5; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java index 62cf7929..e9356fdf 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java @@ -14,6 +14,11 @@ import java.util.ArrayList; import java.util.Date; public class ModToolSanctionMuteEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 2000; + } + @Override public void handle() throws Exception { int userId = this.packet.readInt(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java index 2666f6e6..0cece3ce 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java @@ -12,6 +12,11 @@ import gnu.trove.map.hash.THashMap; import java.util.ArrayList; public class ModToolSanctionTradeLockEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 2000; + } + @Override public void handle() throws Exception { int userId = this.packet.readInt(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolWarnEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolWarnEvent.java index e5f0b102..a8dae64f 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolWarnEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolWarnEvent.java @@ -8,6 +8,11 @@ import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.plugin.events.support.SupportUserAlertedReason; public class ModToolWarnEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 2000; + } + @Override public void handle() throws Exception { if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/RequestCreateRoomEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/RequestCreateRoomEvent.java index 79eebb68..66d0480b 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/RequestCreateRoomEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/RequestCreateRoomEvent.java @@ -13,6 +13,10 @@ import org.slf4j.LoggerFactory; public class RequestCreateRoomEvent extends MessageHandler { private static final Logger LOGGER = LoggerFactory.getLogger(RequestCreateRoomEvent.class); + @Override + public int getRatelimit() { + return 3000; + } @Override public void handle() throws Exception { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RequestRoomLoadEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RequestRoomLoadEvent.java index b5cfffa9..45e2cf47 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RequestRoomLoadEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RequestRoomLoadEvent.java @@ -2,18 +2,38 @@ package com.eu.habbo.messages.incoming.rooms; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.messages.incoming.MessageHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class RequestRoomLoadEvent extends MessageHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(RequestRoomLoadEvent.class); + @Override public void handle() throws Exception { int roomId = this.packet.readInt(); String password = this.packet.readString(); + // Optional spawn coordinates from the client (for future reconnection support). + int spawnX = -1; + int spawnY = -1; + + try { + int remaining = this.packet.getBuffer().readableBytes(); + if (remaining >= 8) { + spawnX = this.packet.readInt(); + spawnY = this.packet.readInt(); + } + } catch (Exception e) { + spawnX = -1; + spawnY = -1; + } + // Reset stale loadingRoom if timestamp has expired (indicates failed/stuck load) - if (this.client.getHabbo().getHabboInfo().getLoadingRoom() != 0 - && this.client.getHabbo().getHabboStats().roomEnterTimestamp + 5000 < System.currentTimeMillis()) { + if (this.client.getHabbo().getHabboInfo().getLoadingRoom() != 0 + && this.client.getHabbo().getHabboStats().roomEnterTimestamp + 5000 < System.currentTimeMillis()) { this.client.getHabbo().getHabboInfo().setLoadingRoom(0); } @@ -30,6 +50,18 @@ public class RequestRoomLoadEvent extends MessageHandler { Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room != null) { + // If re-entering the same room (session resume / reconnect), capture + // the user's current position before removal so we can respawn there. + if (room.getId() == roomId && spawnX < 0 && spawnY < 0 + && this.client.getHabbo().getRoomUnit() != null + && this.client.getHabbo().getRoomUnit().getCurrentLocation() != null) { + RoomTile currentLoc = this.client.getHabbo().getRoomUnit().getCurrentLocation(); + spawnX = currentLoc.x; + spawnY = currentLoc.y; + LOGGER.info("[RequestRoomLoadEvent] Re-entering same room {} — preserving position ({}, {})", + roomId, spawnX, spawnY); + } + Emulator.getGameEnvironment().getRoomManager().logExit(this.client.getHabbo()); room.removeHabbo(this.client.getHabbo(), true); @@ -41,7 +73,28 @@ public class RequestRoomLoadEvent extends MessageHandler { this.client.getHabbo().getRoomUnit().isTeleporting = false; } - Emulator.getGameEnvironment().getRoomManager().enterRoom(this.client.getHabbo(), roomId, password); + // Resolve spawn tile from coordinates (either from client or from saved position above) + RoomTile spawnTile = null; + + if (spawnX >= 0 && spawnY >= 0) { + Room targetRoom = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId); + if (targetRoom == null) { + targetRoom = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId); + } + if (targetRoom != null && targetRoom.getLayout() != null) { + RoomTile tile = targetRoom.getLayout().getTile((short) spawnX, (short) spawnY); + if (tile != null && tile.isWalkable()) { + spawnTile = tile; + } + } + } + + boolean isReconnect = spawnTile != null; + LOGGER.debug("[RequestRoomLoadEvent] Entering room {} (spawnTile={}, isReconnect={})", + roomId, + spawnTile != null ? "(" + spawnTile.x + "," + spawnTile.y + ")" : "door", + isReconnect); + Emulator.getGameEnvironment().getRoomManager().enterRoom(this.client.getHabbo(), roomId, password, false, spawnTile, isReconnect); } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ClickFurniEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ClickFurniEvent.java new file mode 100644 index 00000000..640d0ce0 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ClickFurniEvent.java @@ -0,0 +1,43 @@ +package com.eu.habbo.messages.incoming.rooms.items; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.incoming.MessageHandler; + +public class ClickFurniEvent extends MessageHandler { + private static final String CLICK_TILE_INTERACTION = "room_invisible_click_tile"; + + @Override + public void handle() throws Exception { + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + + if (room == null) { + return; + } + + int itemId = Math.abs(this.packet.readInt()); + this.packet.readInt(); + + HabboItem item = room.getHabboItem(itemId); + + if (item == null) { + return; + } + + WiredManager.queueUserClicksFurni(room, this.client.getHabbo().getRoomUnit(), item); + + if (isClickTileItem(item)) { + WiredManager.triggerUserClicksTile(room, this.client.getHabbo().getRoomUnit(), item); + } + } + + private boolean isClickTileItem(HabboItem item) { + if (item == null || item.getBaseItem() == null || item.getBaseItem().getInteractionType() == null) { + return false; + } + + String interaction = item.getBaseItem().getInteractionType().getName(); + return interaction != null && interaction.equalsIgnoreCase(CLICK_TILE_INTERACTION); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java index 0f293184..1b23813b 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java @@ -3,9 +3,12 @@ package com.eu.habbo.messages.incoming.rooms.items; import com.eu.habbo.habbohotel.items.FurnitureType; import com.eu.habbo.habbohotel.items.interactions.*; import com.eu.habbo.habbohotel.modtool.ScripterManager; +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; import com.eu.habbo.habbohotel.rooms.*; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.catalog.BuildersClubFurniCountComposer; +import com.eu.habbo.messages.outgoing.catalog.BuildersClubSubscriptionStatusComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; import com.eu.habbo.messages.outgoing.inventory.RemoveHabboItemComposer; @@ -77,6 +80,7 @@ public class RoomPlaceItemEvent extends MessageHandler { if ((rentSpace != null || buildArea != null) && !room.hasRights(this.client.getHabbo())) { if (item instanceof InteractionRoller || item instanceof InteractionStackHelper || + item instanceof InteractionStackWalkHelper || item instanceof InteractionWired || item instanceof InteractionBackgroundToner || item instanceof InteractionRoomAds || @@ -114,5 +118,29 @@ public class RoomPlaceItemEvent extends MessageHandler { this.client.sendResponse(new RemoveHabboItemComposer(item.getGiftAdjustedId())); this.client.getHabbo().getInventory().getItemsComponent().removeHabboItem(item.getId()); item.setFromGift(false); + + if (BuildersClubRoomSupport.isTrackedItem(item.getId())) { + int trackedUserId = BuildersClubRoomSupport.getTrackedUserId(item.getId()); + + if (trackedUserId <= 0) { + trackedUserId = this.client.getHabbo().getHabboInfo().getId(); + } + + item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID); + BuildersClubRoomSupport.trackPlacedItem(item.getId(), trackedUserId, room.getId()); + + BuildersClubRoomSupport.SyncResult syncResult = BuildersClubRoomSupport.syncRoom(room); + + if (syncResult == BuildersClubRoomSupport.SyncResult.LOCKED) { + BuildersClubRoomSupport.sendRoomLockedBubble(room.getOwnerId()); + } else if (syncResult == BuildersClubRoomSupport.SyncResult.UNLOCKED) { + BuildersClubRoomSupport.sendRoomUnlockedBubble(room.getOwnerId()); + } + + if (trackedUserId == this.client.getHabbo().getHabboInfo().getId()) { + this.client.sendResponse(new BuildersClubFurniCountComposer(BuildersClubRoomSupport.getTrackedFurniCount(trackedUserId))); + this.client.sendResponse(new BuildersClubSubscriptionStatusComposer(this.client.getHabbo())); + } + } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/SetStackHelperHeightEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/SetStackHelperHeightEvent.java index edc29cc2..c3b1d337 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/SetStackHelperHeightEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/SetStackHelperHeightEvent.java @@ -1,6 +1,7 @@ package com.eu.habbo.messages.incoming.rooms.items; import com.eu.habbo.habbohotel.items.interactions.InteractionStackHelper; +import com.eu.habbo.habbohotel.items.interactions.InteractionStackWalkHelper; import com.eu.habbo.habbohotel.items.interactions.InteractionTileWalkMagic; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomTile; @@ -21,7 +22,7 @@ public class SetStackHelperHeightEvent extends MessageHandler { if (this.client.getHabbo().getHabboInfo().getId() == this.client.getHabbo().getHabboInfo().getCurrentRoom().getOwnerId() || this.client.getHabbo().getHabboInfo().getCurrentRoom().hasRights(this.client.getHabbo())) { HabboItem item = this.client.getHabbo().getHabboInfo().getCurrentRoom().getHabboItem(itemId); - if (item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic) { + if (item instanceof InteractionStackHelper || item instanceof InteractionTileWalkMagic || item instanceof InteractionStackWalkHelper) { Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); RoomTile itemTile = room.getLayout().getTile(item.getX(), item.getY()); double stackerHeight = this.packet.readInt(); @@ -51,7 +52,7 @@ public class SetStackHelperHeightEvent extends MessageHandler { item.setExtradata((int) (height * 100) + ""); item.needsUpdate(true); - if (item instanceof InteractionTileWalkMagic) { + if (item instanceof InteractionTileWalkMagic || item instanceof InteractionStackWalkHelper) { for (RoomTile t : tiles) { this.client.getHabbo().getHabboInfo().getCurrentRoom().updateHabbosAt(t.x, t.y); this.client.getHabbo().getHabboInfo().getCurrentRoom().updateBotsAt(t.x, t.y); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleFloorItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleFloorItemEvent.java index 3cf6eec0..e81a651b 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleFloorItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleFloorItemEvent.java @@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.items.interactions.pets.InteractionMonsterPlantSe import com.eu.habbo.habbohotel.pets.MonsterplantPet; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.rooms.items.RemoveFloorItemComposer; import com.eu.habbo.messages.outgoing.rooms.pets.PetPackageComposer; @@ -40,6 +41,8 @@ public class ToggleFloorItemEvent extends MessageHandler { if (item == null || item instanceof InteractionDice) return; + WiredManager.cancelPendingUserClicksFurni(room, this.client.getHabbo().getRoomUnit(), item); + Event furnitureToggleEvent = new FurnitureToggleEvent(item, this.client.getHabbo(), state); Emulator.getPluginManager().fireEvent(furnitureToggleEvent); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleWallItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleWallItemEvent.java index d4020edf..b24b6f66 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleWallItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleWallItemEvent.java @@ -3,6 +3,7 @@ package com.eu.habbo.messages.incoming.rooms.items; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.plugin.Event; import com.eu.habbo.plugin.events.furniture.FurnitureToggleEvent; @@ -23,6 +24,8 @@ public class ToggleWallItemEvent extends MessageHandler { if (item == null) return; + WiredManager.cancelPendingUserClicksFurni(room, this.client.getHabbo().getRoomUnit(), item); + Event furnitureToggleEvent = new FurnitureToggleEvent(item, this.client.getHabbo(), state); Emulator.getPluginManager().fireEvent(furnitureToggleEvent); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/UpdateFurniturePositionEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/UpdateFurniturePositionEvent.java index ee0b9ae0..f5bce826 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/UpdateFurniturePositionEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/UpdateFurniturePositionEvent.java @@ -7,8 +7,12 @@ import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; +import com.eu.habbo.messages.outgoing.rooms.WiredMovementsComposer; import com.eu.habbo.messages.outgoing.rooms.items.FloorItemUpdateComposer; +import java.util.ArrayList; +import java.util.List; + public class UpdateFurniturePositionEvent extends MessageHandler { @Override public void handle() throws Exception { @@ -34,10 +38,30 @@ public class UpdateFurniturePositionEvent extends MessageHandler { return; } - error = room.moveFurniTo(item, tile, rotation, z, this.client.getHabbo(), true, true); + RoomTile oldTile = room.getLayout().getTile(item.getX(), item.getY()); + double oldZ = item.getZ(); + + error = room.moveFurniTo(item, tile, rotation, z, this.client.getHabbo(), false, true); if (error != FurnitureMovementError.NONE) { this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, error.errorCode)); this.client.sendResponse(new FloorItemUpdateComposer(item)); + return; + } + + if (oldTile != null) { + List movements = new ArrayList<>(1); + movements.add(WiredMovementsComposer.furniMovement( + item.getId(), + oldTile.x, + oldTile.y, + tile.x, + tile.y, + oldZ, + item.getZ(), + item.getRotation(), + WiredMovementsComposer.DEFAULT_DURATION)); + + room.sendComposer(new WiredMovementsComposer(movements).compose()); } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylistChange.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylistChange.java index 437298dd..fbc5b7e1 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylistChange.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylistChange.java @@ -34,7 +34,7 @@ public class YoutubeRequestPlaylistChange extends MessageHandler { if (item == null || !(item instanceof InteractionYoutubeTV)) return; - Optional playlist = Emulator.getGameEnvironment().getItemManager().getYoutubeManager().getPlaylistsForItemId(item.getBaseItem().getId()).stream().filter(p -> p.getId().equals(playlistId)).findAny(); + Optional playlist = Emulator.getGameEnvironment().getItemManager().getYoutubeManager().getPlaylistsForItemId(item.getId()).stream().filter(p -> p.getId().equals(playlistId)).findAny(); if (playlist.isPresent()) { YoutubeManager.YoutubeVideo video = playlist.get().getVideos().get(0); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylists.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylists.java index 7121f58e..8b480bf5 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylists.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylists.java @@ -25,10 +25,11 @@ public class YoutubeRequestPlaylists extends MessageHandler { if (item instanceof InteractionYoutubeTV) { InteractionYoutubeTV tv = (InteractionYoutubeTV) item; - ArrayList playlists = Emulator.getGameEnvironment().getItemManager().getYoutubeManager().getPlaylistsForItemId(item.getBaseItem().getId()); + int furniItemId = item.getId(); + ArrayList playlists = Emulator.getGameEnvironment().getItemManager().getYoutubeManager().getPlaylistsForItemId(furniItemId); if (playlists == null) { - LOGGER.error("No YouTube playlists set for base item #{}", item.getBaseItem().getId()); + LOGGER.error("No YouTube playlists loaded for item #{}. Check: 1) youtube_playlists table has entries with item_id={}, 2) youtube.apikey is set in emulator_settings, 3) playlist IDs are valid YouTube playlist IDs (start with PL)", furniItemId, furniItemId); this.client.sendResponse(new ConnectionErrorComposer(1000)); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/ClickUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/ClickUserEvent.java new file mode 100644 index 00000000..d8c0b2f2 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/ClickUserEvent.java @@ -0,0 +1,43 @@ +package com.eu.habbo.messages.incoming.rooms.users; + +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.habbohotel.items.interactions.wired.triggers.WiredTriggerHabboClicksUser; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.users.InClientLinkComposer; + +public class ClickUserEvent extends MessageHandler { + @Override + public void handle() throws Exception { + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + + if (room == null) { + return; + } + + RoomUnit clickingUser = this.client.getHabbo().getRoomUnit(); + + if (clickingUser == null) { + return; + } + + int roomUnitId = this.packet.readInt(); + Habbo clickedHabbo = room.getHabboByRoomUnitId(roomUnitId); + + if (clickedHabbo == null || clickedHabbo.getRoomUnit() == null) { + return; + } + + WiredManager.triggerUserClicksUser(room, clickingUser, clickedHabbo.getRoomUnit()); + + if (WiredTriggerHabboClicksUser.hasPendingIgnoreLook(clickingUser)) { + this.client.sendResponse(new InClientLinkComposer("avatar-info/block-rotate")); + } + + if (WiredTriggerHabboClicksUser.consumeBlockMenuOpen(clickingUser)) { + this.client.sendResponse(new InClientLinkComposer("avatar-info/block-menu")); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserActionEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserActionEvent.java index 41c29f25..bc32465b 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserActionEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserActionEvent.java @@ -4,6 +4,8 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUserAction; import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.wired.WiredUserActionType; +import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserActionComposer; import com.eu.habbo.plugin.events.users.UserIdleEvent; @@ -26,6 +28,7 @@ public class RoomUserActionEvent extends MessageHandler { } int action = this.packet.readInt(); + int wiredAction = 0; if (action == 5) { UserIdleEvent event = new UserIdleEvent(this.client.getHabbo(), UserIdleEvent.IdleReason.ACTION, true); @@ -34,8 +37,10 @@ public class RoomUserActionEvent extends MessageHandler { if (!event.isCancelled()) { if (event.idle) { room.idle(habbo); + wiredAction = WiredUserActionType.RELAX; } else { room.unIdle(habbo); + wiredAction = WiredUserActionType.AWAKE; } } } else { @@ -51,6 +56,29 @@ public class RoomUserActionEvent extends MessageHandler { } room.sendComposer(new RoomUserActionComposer(habbo.getRoomUnit(), RoomUserAction.fromValue(action)).compose()); + + if (wiredAction == 0) { + switch (action) { + case 1: + wiredAction = WiredUserActionType.WAVE; + break; + case 2: + wiredAction = WiredUserActionType.BLOW_KISS; + break; + case 3: + wiredAction = WiredUserActionType.LAUGH; + break; + case 7: + wiredAction = WiredUserActionType.THUMB_UP; + break; + default: + break; + } + } + + if (wiredAction != 0) { + WiredManager.triggerUserPerformsAction(room, habbo.getRoomUnit(), wiredAction, -1); + } } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserDanceEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserDanceEvent.java index bafd54dd..bda9bd4e 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserDanceEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserDanceEvent.java @@ -3,8 +3,9 @@ package com.eu.habbo.messages.incoming.rooms.users; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.users.DanceType; import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.wired.WiredUserActionType; +import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.incoming.MessageHandler; -import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDanceComposer; import com.eu.habbo.plugin.events.users.UserIdleEvent; public class RoomUserDanceEvent extends MessageHandler { @@ -14,7 +15,7 @@ public class RoomUserDanceEvent extends MessageHandler { return; int danceId = this.packet.readInt(); - if (danceId >= 0 && danceId <= 5) { + if (danceId >= 0 && danceId <= 4) { if (this.client.getHabbo().getRoomUnit().isInRoom()) { Habbo habbo = this.client.getHabbo(); @@ -29,8 +30,6 @@ public class RoomUserDanceEvent extends MessageHandler { } } - habbo.getRoomUnit().setDanceType(DanceType.values()[danceId]); - UserIdleEvent event = new UserIdleEvent(this.client.getHabbo(), UserIdleEvent.IdleReason.DANCE, false); Emulator.getPluginManager().fireEvent(event); @@ -40,7 +39,11 @@ public class RoomUserDanceEvent extends MessageHandler { } } - this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDanceComposer(habbo.getRoomUnit()).compose()); + this.client.getHabbo().getHabboInfo().getCurrentRoom().dance(habbo, DanceType.values()[danceId]); + + if (danceId > 0) { + WiredManager.triggerUserPerformsAction(this.client.getHabbo().getHabboInfo().getCurrentRoom(), habbo.getRoomUnit(), WiredUserActionType.DANCE, danceId); + } } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserGiveHandItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserGiveHandItemEvent.java index 9342e045..dd6333f7 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserGiveHandItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserGiveHandItemEvent.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.rooms.users; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomHanditemBlockSupport; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.threading.runnables.HabboGiveHandItemToHabbo; @@ -18,6 +19,10 @@ public class RoomUserGiveHandItemEvent extends MessageHandler { Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room != null) { + if (RoomHanditemBlockSupport.isHanditemBlocked(room)) { + return; + } + Habbo target = room.getHabbo(userId); if (target != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserLookAtPoint.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserLookAtPoint.java index 473b362c..50fc065d 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserLookAtPoint.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserLookAtPoint.java @@ -4,6 +4,7 @@ import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; +import com.eu.habbo.habbohotel.items.interactions.wired.triggers.WiredTriggerHabboClicksUser; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.incoming.MessageHandler; @@ -40,6 +41,9 @@ public class RoomUserLookAtPoint extends MessageHandler { if (roomUnit.isIdle()) return; + if (WiredTriggerHabboClicksUser.consumeIgnoreLook(roomUnit)) + return; + int x = this.packet.readInt(); int y = this.packet.readInt(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserSignEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserSignEvent.java index 3b2aaf93..fba5fccf 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserSignEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserSignEvent.java @@ -5,6 +5,8 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionVoteCounter; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredUserActionType; +import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.plugin.events.users.UserSignEvent; @@ -22,6 +24,7 @@ public class RoomUserSignEvent extends MessageHandler { if (!Emulator.getPluginManager().fireEvent(event).isCancelled()) { this.client.getHabbo().getRoomUnit().setStatus(RoomUnitStatus.SIGN, event.sign + ""); this.client.getHabbo().getHabboInfo().getCurrentRoom().unIdle(this.client.getHabbo()); + WiredManager.triggerUserPerformsAction(room, this.client.getHabbo().getRoomUnit(), WiredUserActionType.SIGN, event.sign); if(signId <= 10) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserSitEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserSitEvent.java index f3c8df7f..debdb49c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserSitEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserSitEvent.java @@ -7,11 +7,18 @@ import com.eu.habbo.plugin.events.users.UserIdleEvent; public class RoomUserSitEvent extends MessageHandler { @Override public void handle() throws Exception { + int posture = this.packet.readInt(); + if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { if (this.client.getHabbo().getRoomUnit().isWalking()) { this.client.getHabbo().getRoomUnit().stopWalking(); } - this.client.getHabbo().getHabboInfo().getCurrentRoom().makeSit(this.client.getHabbo()); + + if (posture == 0) { + this.client.getHabbo().getHabboInfo().getCurrentRoom().makeStand(this.client.getHabbo()); + } else { + this.client.getHabbo().getHabboInfo().getCurrentRoom().makeSit(this.client.getHabbo()); + } UserIdleEvent event = new UserIdleEvent(this.client.getHabbo(), UserIdleEvent.IdleReason.WALKED, false); Emulator.getPluginManager().fireEvent(event); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java index 2b5d5a5e..51a38650 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java @@ -10,8 +10,12 @@ import com.eu.habbo.habbohotel.rooms.BedProfile; 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.items.interactions.wired.effects.WiredEffectMoveRotateUser; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; +import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.rooms.users.RoomUnitOnRollerComposer; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer; import com.eu.habbo.plugin.events.users.UserIdleEvent; import gnu.trove.set.hash.THashSet; import org.slf4j.Logger; @@ -22,9 +26,16 @@ public class RoomUserWalkEvent extends MessageHandler { private static final Logger LOGGER = LoggerFactory.getLogger(RoomUserWalkEvent.class); public static final String CONTROL_KEY = "control"; + private static final String WALK_FLOOD_COUNT_KEY = "__walkFloodCount"; + private static final String WALK_FLOOD_WINDOW_KEY = "__walkFloodWindow"; + private static final String WALK_LAST_X_KEY = "__walkLastX"; + private static final String WALK_LAST_Y_KEY = "__walkLastY"; + + private static final int MAX_WALKS_PER_SECOND = 15; + @Override public int getRatelimit() { - return Emulator.getConfig().getInt("pathfinder.click.delay", 0); + return 0; } @Override @@ -33,8 +44,43 @@ public class RoomUserWalkEvent extends MessageHandler { return; } - int x = this.packet.readInt(); // Position X - int y = this.packet.readInt(); // Position Y + int x = this.packet.readInt(); + int y = this.packet.readInt(); + + RoomUnit unit = this.client.getHabbo().getRoomUnit(); + if (unit != null) { + long now = System.currentTimeMillis(); + Object windowObj = unit.getCacheable().get(WALK_FLOOD_WINDOW_KEY); + Object countObj = unit.getCacheable().get(WALK_FLOOD_COUNT_KEY); + + long windowStart = (windowObj instanceof Long) ? (Long) windowObj : 0L; + int count = (countObj instanceof Integer) ? (Integer) countObj : 0; + + if (now - windowStart > 1000) { + // New 1-second window + windowStart = now; + count = 0; + } + + count++; + unit.getCacheable().put(WALK_FLOOD_WINDOW_KEY, windowStart); + unit.getCacheable().put(WALK_FLOOD_COUNT_KEY, count); + + if (count > MAX_WALKS_PER_SECOND) { + unit.getCacheable().put(WALK_LAST_X_KEY, x); + unit.getCacheable().put(WALK_LAST_Y_KEY, y); + return; + } + + Object lastX = unit.getCacheable().get(WALK_LAST_X_KEY); + Object lastY = unit.getCacheable().get(WALK_LAST_Y_KEY); + if (lastX != null && lastY != null) { + x = (Integer) lastX; + y = (Integer) lastY; + unit.getCacheable().remove(WALK_LAST_X_KEY); + unit.getCacheable().remove(WALK_LAST_Y_KEY); + } + } Habbo habbo = getControlledHabbo(); if (habbo == null) { @@ -46,7 +92,11 @@ public class RoomUserWalkEvent extends MessageHandler { Room room = habboInfo.getCurrentRoom(); try { - if (roomUnit != null && roomUnit.isInRoom() && roomUnit.canWalk()) { + if (roomUnit != null && roomUnit.isInRoom() && roomUnit.canWalk() && !WiredFreezeUtil.isFrozen(roomUnit)) { + if (WiredUserMovementHelper.consumeSuppressedWalkCommand(roomUnit)) { + return; + } + if (roomUnit.cmdTeleport) { handleTeleport(room, (short) x, (short) y, roomUnit, habboInfo); return; @@ -108,10 +158,30 @@ public class RoomUserWalkEvent extends MessageHandler { // This is where we set the end location and begin finding a path if (tile.isWalkable() || room.canSitOrLayAt(tile.x, tile.y)) { + if (WiredEffectMoveRotateUser.handleWalkWhileActive(room, roomUnit, tile)) { + return; + } + if (roomUnit.getMoveBlockingTask() != null) { roomUnit.getMoveBlockingTask().get(); } + boolean needsLocationResync = + roomUnit.getCurrentLocation() != null + && (roomUnit.getPreviousLocation() == null + || roomUnit.getPreviousLocation().x != roomUnit.getCurrentLocation().x + || roomUnit.getPreviousLocation().y != roomUnit.getCurrentLocation().y + || Math.abs(roomUnit.getPreviousLocationZ() - roomUnit.getZ()) > 0.01D); + + if (WiredUserMovementHelper.shouldSuppressStatusComposer(roomUnit) || needsLocationResync) { + WiredUserMovementHelper.clearStatusComposerSuppression(roomUnit); + if (roomUnit.getCurrentLocation() != null) { + roomUnit.setPreviousLocation(roomUnit.getCurrentLocation()); + roomUnit.setPreviousLocationZ(roomUnit.getZ()); + } + room.sendComposer(new RoomUserStatusComposer(roomUnit).compose()); + } + roomUnit.setGoalLocation(tile); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomPlayEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomPlayEvent.java new file mode 100644 index 00000000..1dec152a --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomPlayEvent.java @@ -0,0 +1,63 @@ +package com.eu.habbo.messages.incoming.rooms.youtube; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomBroadcastComposer; + +import java.util.ArrayList; +import java.util.List; + +public class YouTubeRoomPlayEvent extends MessageHandler { + + private static final int MAX_VIDEO_ID_LENGTH = 100; + private static final int MAX_PLAYLIST_ITEM_LENGTH = 200; + private static final int MAX_PLAYLIST_SIZE = 50; + + @Override + public int getRatelimit() { + // Max 1 broadcast every 2 seconds per client + return 2000; + } + + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + if (habbo == null) return; + + Room room = habbo.getHabboInfo().getCurrentRoom(); + if (room == null) return; + if (!room.isYoutubeEnabled()) return; + if (!room.isOwner(habbo) && !room.hasRights(habbo)) return; + + String videoId = this.packet.readString(); + if (videoId.length() > MAX_VIDEO_ID_LENGTH) { + videoId = videoId.substring(0, MAX_VIDEO_ID_LENGTH); + } + + int playlistCount = this.packet.readInt(); + if (playlistCount > MAX_PLAYLIST_SIZE) playlistCount = MAX_PLAYLIST_SIZE; + if (playlistCount < 0) playlistCount = 0; + + List playlist = new ArrayList<>(); + for (int i = 0; i < playlistCount; i++) { + String item = this.packet.readString(); + if (item.length() > MAX_PLAYLIST_ITEM_LENGTH) { + item = item.substring(0, MAX_PLAYLIST_ITEM_LENGTH); + } + playlist.add(item); + } + + // Store the current video + playlist on the room, or clear if empty + if (videoId.isEmpty()) { + room.clearYoutubeVideo(); + } else { + room.setYoutubeVideo(videoId, habbo.getHabboInfo().getUsername(), playlist); + } + + // Broadcast to everyone in the room (empty videoId = stop) + room.sendComposer( + new YouTubeRoomBroadcastComposer(videoId, habbo.getHabboInfo().getUsername(), playlist).compose() + ); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomSettingsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomSettingsEvent.java new file mode 100644 index 00000000..b3f8ceb5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomSettingsEvent.java @@ -0,0 +1,35 @@ +package com.eu.habbo.messages.incoming.rooms.youtube; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomBroadcastComposer; +import com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomSettingsComposer; + +public class YouTubeRoomSettingsEvent extends MessageHandler { + + @Override + public int getRatelimit() { + return 200; + } + + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + if (habbo == null) return; + + Room room = habbo.getHabboInfo().getCurrentRoom(); + if (room == null) return; + if (!room.isOwner(habbo)) return; + + boolean enabled = this.packet.readInt() == 1; + room.setYoutubeEnabled(enabled); + room.setNeedsUpdate(true); + room.sendComposer(new YouTubeRoomSettingsComposer(enabled).compose()); + + if (!enabled && !room.getYoutubeCurrentVideo().isEmpty()) { + room.clearYoutubeVideo(); + room.sendComposer(new YouTubeRoomBroadcastComposer("", "", java.util.Collections.emptyList()).compose()); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomWatchingEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomWatchingEvent.java new file mode 100644 index 00000000..3de11651 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/youtube/YouTubeRoomWatchingEvent.java @@ -0,0 +1,39 @@ +package com.eu.habbo.messages.incoming.rooms.youtube; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomWatchersComposer; + +public class YouTubeRoomWatchingEvent extends MessageHandler { + + @Override + public int getRatelimit() { + return 500; + } + + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + if (habbo == null) return; + + Room room = habbo.getHabboInfo().getCurrentRoom(); + if (room == null) return; + + boolean watching = this.packet.readInt() == 1; + int userId = habbo.getHabboInfo().getId(); + + boolean changed; + if (watching) { + changed = room.getYoutubeWatchers().add(userId); + } else { + changed = room.getYoutubeWatchers().remove(userId); + } + + if (changed) { + room.sendComposer( + new YouTubeRoomWatchersComposer(room.getYoutubeWatchers()).compose() + ); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UserSaveLookEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UserSaveLookEvent.java index c4c30c5f..96642103 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UserSaveLookEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UserSaveLookEvent.java @@ -43,6 +43,11 @@ public class UserSaveLookEvent extends MessageHandler { if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose()); } + this.client.getHabbo().getMessenger().connectionChanged( + this.client.getHabbo(), + this.client.getHabbo().isOnline(), + this.client.getHabbo().getHabboInfo().getCurrentRoom() != null + ); AchievementManager.progressAchievement(this.client.getHabbo(), Emulator.getGameEnvironment().getAchievementManager().getAchievement("AvatarLooks")); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredApplySetConditionsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredApplySetConditionsEvent.java index 1d9266f3..28bceeb0 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredApplySetConditionsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredApplySetConditionsEvent.java @@ -14,6 +14,7 @@ import com.eu.habbo.messages.outgoing.rooms.items.FloorItemOnRollerComposer; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.math.BigDecimal; public class WiredApplySetConditionsEvent extends MessageHandler { @@ -40,7 +41,7 @@ public class WiredApplySetConditionsEvent extends MessageHandler { if (room != null) { // Executing Habbo should be able to edit wireds - if (room.hasRights(this.client.getHabbo()) || room.isOwner(this.client.getHabbo())) { + if (room.canModifyWired(this.client.getHabbo())) { List wireds = new ArrayList<>(); wireds.addAll(room.getRoomSpecialTypes().getConditions()); @@ -81,13 +82,21 @@ public class WiredApplySetConditionsEvent extends MessageHandler { room.moveFurniTo(matchItem, oldLocation, setting.rotation, null, true); } } + else if(wired.shouldMatchAltitude() && !wired.shouldMatchPosition()) { + int newRotation = wired.shouldMatchRotation() ? setting.rotation : matchItem.getRotation(); + if (BigDecimal.valueOf(matchItem.getZ()).compareTo(BigDecimal.valueOf(setting.z)) != 0 + || newRotation != matchItem.getRotation()) { + room.moveFurniTo(matchItem, oldLocation, newRotation, setting.z, null, true, false); + } + } else if(wired.shouldMatchPosition()) { boolean slideAnimation = !wired.shouldMatchRotation() || matchItem.getRotation() == setting.rotation; RoomTile newLocation = room.getLayout().getTile((short) setting.x, (short) setting.y); int newRotation = wired.shouldMatchRotation() ? setting.rotation : matchItem.getRotation(); + double newZ = wired.shouldMatchAltitude() ? setting.z : matchItem.getZ(); if(newLocation != null && newLocation.state != RoomTileState.INVALID && (newLocation != oldLocation || newRotation != matchItem.getRotation()) && room.furnitureFitsAt(newLocation, matchItem, newRotation, true) == FurnitureMovementError.NONE) { - if(room.moveFurniTo(matchItem, newLocation, newRotation, null, !slideAnimation) == FurnitureMovementError.NONE) { + if(room.moveFurniTo(matchItem, newLocation, newRotation, newZ, null, !slideAnimation, true) == FurnitureMovementError.NONE) { if(slideAnimation) { room.sendComposer(new FloorItemOnRollerComposer(matchItem, null, oldLocation, oldZ, newLocation, matchItem.getZ(), 0, room).compose()); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredConditionSaveDataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredConditionSaveDataEvent.java index b5e51094..c1ca5c37 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredConditionSaveDataEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredConditionSaveDataEvent.java @@ -4,7 +4,6 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.interactions.InteractionWired; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredCondition; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; -import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.incoming.MessageHandler; @@ -23,7 +22,7 @@ public class WiredConditionSaveDataEvent extends MessageHandler { Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room != null) { - if (room.hasRights(this.client.getHabbo()) || room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER) || this.client.getHabbo().hasPermission(Permission.ACC_MOVEROTATE)) { + if (room.canModifyWired(this.client.getHabbo())) { InteractionWiredCondition condition = room.getRoomSpecialTypes().getCondition(itemId); if (condition != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java index aac7ec7a..258e02ce 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java @@ -3,18 +3,14 @@ package com.eu.habbo.messages.incoming.wired; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.interactions.InteractionWired; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; -import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.generic.alerts.UpdateFailedComposer; import com.eu.habbo.messages.outgoing.wired.WiredSavedComposer; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Optional; - public class WiredEffectSaveDataEvent extends MessageHandler { @Override public void handle() throws Exception { @@ -23,42 +19,38 @@ public class WiredEffectSaveDataEvent extends MessageHandler { Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room != null) { - if (room.hasRights(this.client.getHabbo()) || room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER) || this.client.getHabbo().hasPermission(Permission.ACC_MOVEROTATE)) { + if (room.canModifyWired(this.client.getHabbo())) { InteractionWiredEffect effect = room.getRoomSpecialTypes().getEffect(itemId); + InteractionWiredExtra extra = room.getRoomSpecialTypes().getExtra(itemId); try { - if (effect == null) - throw new WiredSaveException(String.format("Wired effect with item id %s not found in room", itemId)); + if (effect == null && extra == null) + throw new WiredSaveException(String.format("Wired effect/extra with item id %s not found in room", itemId)); - Optional saveMethod = Arrays.stream(effect.getClass().getMethods()).filter(x -> x.getName().equals("saveData")).findFirst(); + WiredSettings settings = InteractionWired.readSettings(this.packet, true); + boolean saved; - if(saveMethod.isPresent()) { - if(saveMethod.get().getParameterTypes()[0] == WiredSettings.class) { - WiredSettings settings = InteractionWired.readSettings(this.packet, true); - if (effect.saveData(settings, this.client)) { - this.client.sendResponse(new WiredSavedComposer()); - effect.needsUpdate(true); - Emulator.getThreading().run(effect); - - // Invalidate wired cache when effect is saved - WiredManager.invalidateRoom(room); - } - } - else { - if ((boolean) saveMethod.get().invoke(effect, this.packet, this.client)) { - this.client.sendResponse(new WiredSavedComposer()); - effect.needsUpdate(true); - Emulator.getThreading().run(effect); - - // Invalidate wired cache when effect is saved - WiredManager.invalidateRoom(room); - } - } + if (effect != null) { + saved = effect.saveData(settings, this.client); } else { - this.client.sendResponse(new UpdateFailedComposer("Save method was not found")); + saved = extra.saveData(settings, this.client); } + if (saved) { + this.client.sendResponse(new WiredSavedComposer()); + if (effect != null) { + effect.needsUpdate(true); + Emulator.getThreading().run(effect); + } else { + extra.needsUpdate(true); + Emulator.getThreading().run(extra); + } + // Invalidate wired cache when effect is saved + WiredManager.invalidateRoom(room); + } else { + this.client.sendResponse(new UpdateFailedComposer("There was an error while saving that effect")); + } } catch (WiredSaveException e) { this.client.sendResponse(new UpdateFailedComposer(e.getMessage())); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredMonitorRequestEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredMonitorRequestEvent.java new file mode 100644 index 00000000..2be8c2c7 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredMonitorRequestEvent.java @@ -0,0 +1,41 @@ +package com.eu.habbo.messages.incoming.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.wired.WiredMonitorDataComposer; + +public class WiredMonitorRequestEvent extends MessageHandler { + private static final int ACTION_FETCH = 0; + private static final int ACTION_CLEAR_LOGS = 1; + + @Override + public void handle() throws Exception { + Room room = currentRoom(); + + if (room == null) { + return; + } + + if (!room.canInspectWired(this.client.getHabbo())) { + return; + } + + int action = ACTION_FETCH; + + if (this.packet.bytesAvailable() >= 4) { + action = this.packet.readInt(); + } + + if ((action == ACTION_CLEAR_LOGS) && room.canModifyWired(this.client.getHabbo())) { + WiredManager.clearDiagnosticsLogs(room.getId()); + } + + this.client.sendResponse(new WiredMonitorDataComposer(WiredManager.getDiagnosticsSnapshot(room.getId()))); + } + + @Override + public int getRatelimit() { + return 50; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredRoomSettingsRequestEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredRoomSettingsRequestEvent.java new file mode 100644 index 00000000..3275b079 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredRoomSettingsRequestEvent.java @@ -0,0 +1,23 @@ +package com.eu.habbo.messages.incoming.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.wired.WiredRoomSettingsDataComposer; + +public class WiredRoomSettingsRequestEvent extends MessageHandler { + @Override + public void handle() throws Exception { + Room room = currentRoom(); + + if (room == null) { + return; + } + + this.client.sendResponse(new WiredRoomSettingsDataComposer(room, this.client.getHabbo())); + } + + @Override + public int getRatelimit() { + return 250; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredRoomSettingsSaveEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredRoomSettingsSaveEvent.java new file mode 100644 index 00000000..a58246f7 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredRoomSettingsSaveEvent.java @@ -0,0 +1,38 @@ +package com.eu.habbo.messages.incoming.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.wired.WiredRoomSettingsDataComposer; + +public class WiredRoomSettingsSaveEvent extends MessageHandler { + @Override + public void handle() throws Exception { + Room room = currentRoom(); + + if (room == null) { + return; + } + + if (this.packet.bytesAvailable() < 8) { + this.client.sendResponse(new WiredRoomSettingsDataComposer(room, this.client.getHabbo())); + return; + } + + if (!room.canManageWiredSettings(this.client.getHabbo())) { + this.client.sendResponse(new WiredRoomSettingsDataComposer(room, this.client.getHabbo())); + return; + } + + int inspectMask = this.packet.readInt(); + int modifyMask = this.packet.readInt(); + + room.saveWiredSettings(inspectMask, modifyMask); + + this.client.sendResponse(new WiredRoomSettingsDataComposer(room, this.client.getHabbo())); + } + + @Override + public int getRatelimit() { + return 250; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java index 83da31e5..66970dd2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java @@ -4,17 +4,12 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.interactions.InteractionWired; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; -import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.generic.alerts.UpdateFailedComposer; import com.eu.habbo.messages.outgoing.wired.WiredSavedComposer; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Optional; - public class WiredTriggerSaveDataEvent extends MessageHandler { @Override public void handle() throws Exception { @@ -23,44 +18,25 @@ public class WiredTriggerSaveDataEvent extends MessageHandler { Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room != null) { - if (room.hasRights(this.client.getHabbo()) || room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER) || this.client.getHabbo().hasPermission(Permission.ACC_MOVEROTATE)) { + if (room.canModifyWired(this.client.getHabbo())) { InteractionWiredTrigger trigger = room.getRoomSpecialTypes().getTrigger(itemId); if (trigger != null) { + WiredSettings settings = InteractionWired.readSettings(this.packet, false); - Optional saveMethod = Arrays.stream(trigger.getClass().getMethods()).filter(x -> x.getName().equals("saveData")).findFirst(); + try { + boolean saved = trigger.saveData(settings, this.client); - if(saveMethod.isPresent()) { - if (saveMethod.get().getParameterTypes()[0] == WiredSettings.class) { - WiredSettings settings = InteractionWired.readSettings(this.packet, false); - - if (trigger.saveData(settings)) { - this.client.sendResponse(new WiredSavedComposer()); - - trigger.needsUpdate(true); - - Emulator.getThreading().run(trigger); - - // Invalidate wired cache when trigger is saved - WiredManager.invalidateRoom(room); - } else { - this.client.sendResponse(new UpdateFailedComposer("There was an error while saving that trigger")); - } + if (saved) { + this.client.sendResponse(new WiredSavedComposer()); + trigger.needsUpdate(true); + Emulator.getThreading().run(trigger); + WiredManager.invalidateRoom(room); } else { - if ((boolean) saveMethod.get().invoke(trigger, this.packet)) { - this.client.sendResponse(new WiredSavedComposer()); - trigger.needsUpdate(true); - Emulator.getThreading().run(trigger); - - // Invalidate wired cache when trigger is saved - WiredManager.invalidateRoom(room); - } else { - this.client.sendResponse(new UpdateFailedComposer("There was an error while saving that trigger")); - } + this.client.sendResponse(new UpdateFailedComposer("There was an error while saving that trigger")); } - } - else { - this.client.sendResponse(new UpdateFailedComposer("Save method was not found")); + } catch (WiredTriggerSaveException e) { + this.client.sendResponse(new UpdateFailedComposer(e.getMessage())); } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveException.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveException.java new file mode 100644 index 00000000..63fc33a2 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveException.java @@ -0,0 +1,7 @@ +package com.eu.habbo.messages.incoming.wired; + +public class WiredTriggerSaveException extends RuntimeException { + public WiredTriggerSaveException(String message) { + super(message); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserInspectMoveEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserInspectMoveEvent.java new file mode 100644 index 00000000..fd159b9e --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserInspectMoveEvent.java @@ -0,0 +1,83 @@ +package com.eu.habbo.messages.incoming.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomTile; +import com.eu.habbo.habbohotel.rooms.RoomTileState; +import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.rooms.RoomUserRotation; +import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; +import com.eu.habbo.messages.incoming.MessageHandler; + +public class WiredUserInspectMoveEvent extends MessageHandler { + @Override + public void handle() throws Exception { + Room room = currentRoom(); + + if (room == null) { + return; + } + + if (!room.canModifyWired(this.client.getHabbo())) { + return; + } + + if (this.packet.bytesAvailable() < 16) { + return; + } + + int roomUnitId = this.packet.readInt(); + int x = this.packet.readInt(); + int y = this.packet.readInt(); + int direction = this.packet.readInt(); + + RoomUnit roomUnit = resolveRoomUnit(room, roomUnitId); + + if (roomUnit == null || roomUnit.getCurrentLocation() == null || room.getLayout() == null) { + return; + } + + RoomUserRotation targetRotation = RoomUserRotation.fromValue((((direction % 8) + 8) % 8)); + boolean positionChanged = roomUnit.getX() != x || roomUnit.getY() != y; + boolean directionChanged = roomUnit.getBodyRotation() != targetRotation || roomUnit.getHeadRotation() != targetRotation; + + if (!positionChanged) { + if (directionChanged) { + WiredUserMovementHelper.updateUserDirection(room, roomUnit, targetRotation, targetRotation); + } + + return; + } + + RoomTile targetTile = room.getLayout().getTile((short) x, (short) y); + + if (targetTile == null || targetTile.state == RoomTileState.INVALID || targetTile.state == RoomTileState.BLOCKED) { + return; + } + + double targetZ = targetTile.getStackHeight() + ((targetTile.state == RoomTileState.SIT) ? -0.5 : 0); + + if (!WiredUserMovementHelper.moveUser(room, roomUnit, targetTile, targetZ, targetRotation, targetRotation, WiredUserMovementHelper.DEFAULT_ANIMATION_DURATION, false) + && directionChanged) { + WiredUserMovementHelper.updateUserDirection(room, roomUnit, targetRotation, targetRotation); + } + } + + @Override + public int getRatelimit() { + return 100; + } + + private RoomUnit resolveRoomUnit(Room room, int roomUnitId) { + if (room == null || roomUnitId <= 0) { + return null; + } + + for (RoomUnit roomUnit : room.getRoomUnits()) { + if (roomUnit != null && roomUnit.getId() == roomUnitId) { + return roomUnit; + } + } + + return null; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariableManageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariableManageEvent.java new file mode 100644 index 00000000..ca6ab112 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariableManageEvent.java @@ -0,0 +1,74 @@ +package com.eu.habbo.messages.incoming.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.incoming.MessageHandler; + +public class WiredUserVariableManageEvent extends MessageHandler { + private static final int ACTION_ASSIGN = 0; + private static final int ACTION_REMOVE = 1; + private static final int TARGET_ROOM = 3; + + @Override + public void handle() throws Exception { + Room room = currentRoom(); + + if (room == null) { + return; + } + + if (!room.canModifyWired(this.client.getHabbo())) { + room.getRoomVariableManager().sendSnapshot(this.client.getHabbo()); + return; + } + + if (this.packet.bytesAvailable() < 20) { + room.getRoomVariableManager().sendSnapshot(this.client.getHabbo()); + return; + } + + int action = this.packet.readInt(); + int targetType = this.packet.readInt(); + int targetId = this.packet.readInt(); + int definitionItemId = this.packet.readInt(); + int value = this.packet.readInt(); + + switch (targetType) { + case com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectGiveVariable.TARGET_FURNI: + if (action == ACTION_REMOVE) { + room.getFurniVariableManager().removeVariable(targetId, definitionItemId); + } else { + HabboItem furni = room.getHabboItem(targetId); + if (furni != null) { + room.getFurniVariableManager().assignVariable(furni, definitionItemId, value, true); + } + } + break; + case TARGET_ROOM: + if (action == ACTION_REMOVE) { + room.getRoomVariableManager().removeVariable(definitionItemId); + } else { + room.getRoomVariableManager().updateVariableValue(definitionItemId, value); + } + break; + default: + if (action == ACTION_REMOVE) { + room.getUserVariableManager().removeVariable(targetId, definitionItemId); + } else { + Habbo habbo = room.getHabbo(targetId); + if (habbo != null) { + room.getUserVariableManager().assignVariable(habbo, definitionItemId, value, true); + } + } + break; + } + + room.getRoomVariableManager().sendSnapshot(this.client.getHabbo()); + } + + @Override + public int getRatelimit() { + return 150; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariableUpdateEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariableUpdateEvent.java new file mode 100644 index 00000000..9475a014 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariableUpdateEvent.java @@ -0,0 +1,53 @@ +package com.eu.habbo.messages.incoming.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.items.interactions.wired.effects.WiredEffectGiveVariable; +import com.eu.habbo.messages.incoming.MessageHandler; + +public class WiredUserVariableUpdateEvent extends MessageHandler { + private static final int TARGET_ROOM = 3; + + @Override + public void handle() throws Exception { + Room room = currentRoom(); + + if (room == null) { + return; + } + + if (!room.canModifyWired(this.client.getHabbo())) { + room.getUserVariableManager().sendSnapshot(this.client.getHabbo()); + return; + } + + if (this.packet.bytesAvailable() < 16) { + room.getUserVariableManager().sendSnapshot(this.client.getHabbo()); + return; + } + + int targetType = this.packet.readInt(); + int targetId = this.packet.readInt(); + int definitionItemId = this.packet.readInt(); + int value = this.packet.readInt(); + + if (targetType == WiredEffectGiveVariable.TARGET_FURNI) { + room.getFurniVariableManager().updateVariableValue(targetId, definitionItemId, value); + room.getFurniVariableManager().sendSnapshot(this.client.getHabbo()); + return; + } + + if (targetType == TARGET_ROOM) { + room.getRoomVariableManager().updateVariableValue(definitionItemId, value); + room.getRoomVariableManager().sendSnapshot(this.client.getHabbo()); + return; + } + + room.getUserVariableManager().updateVariableValue(targetId, definitionItemId, value); + room.getUserVariableManager().sendSnapshot(this.client.getHabbo()); + } + + @Override + public int getRatelimit() { + return 150; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariablesRequestEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariablesRequestEvent.java new file mode 100644 index 00000000..81ea1c93 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredUserVariablesRequestEvent.java @@ -0,0 +1,22 @@ +package com.eu.habbo.messages.incoming.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.messages.incoming.MessageHandler; + +public class WiredUserVariablesRequestEvent extends MessageHandler { + @Override + public void handle() throws Exception { + Room room = currentRoom(); + + if (room == null) { + return; + } + + room.getUserVariableManager().sendSnapshot(this.client.getHabbo()); + } + + @Override + public int getRatelimit() { + return 50; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java index 173ec13f..9a28a3a6 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java @@ -41,6 +41,7 @@ public class Outgoing { public final static int HotelViewComposer = 122; public final static int UpdateFriendComposer = 2800; public final static int FloorItemUpdateComposer = 3776; + public final static int WiredMovementsComposer = 3999; public final static int RoomAccessDeniedComposer = 878; public final static int GuildFurniWidgetComposer = 3293; public final static int GiftConfigurationComposer = 2234; @@ -119,6 +120,11 @@ public class Outgoing { public final static int EnableNotificationsComposer = 3284; // PRODUCTION-201611291003-338511768 public final static int HallOfFameComposer = 3005; // PRODUCTION-201611291003-338511768 public final static int WiredSavedComposer = 1155; // PRODUCTION-201611291003-338511768 + public final static int WiredMonitorDataComposer = 5101; // CUSTOM + public final static int WiredRoomSettingsDataComposer = 5102; // CUSTOM + public final static int WiredUserVariablesDataComposer = 5103; // CUSTOM + public final static int ConfInvisStateComposer = 5104; // CUSTOM + public final static int AreaHideComposer = 6001; // CUSTOM public final static int RoomPaintComposer = 2454; // PRODUCTION-201611291003-338511768 public final static int MarketplaceConfigComposer = 1823; // PRODUCTION-201611291003-338511768 public final static int AddBotComposer = 1352; // PRODUCTION-201611291003-338511768 @@ -324,6 +330,7 @@ public class Outgoing { public final static int VerifyMobilePhoneCodeWindowComposer = 800; // PRODUCTION-201611291003-338511768 public final static int VerifyMobilePhoneDoneComposer = 91; // PRODUCTION-201611291003-338511768 public final static int RoomUserReceivedHandItemComposer = 354; // PRODUCTION-201611291003-338511768 + public final static int HanditemBlockStateComposer = 5105; public final static int MutedWhisperComposer = 826; // PRODUCTION-201611291003-338511768 public final static int UnknownHintComposer = 1787; // PRODUCTION-201611291003-338511768 public final static int BullyReportClosedComposer = 2674; // PRODUCTION-201611291003-338511768 @@ -369,6 +376,7 @@ public class Outgoing { public final static int BullyReportedMessageComposer = 3285; // PRODUCTION-201611291003-338511768 public final static int UnknownQuestComposer3 = 1122; // PRODUCTION-201611291003-338511768 public final static int FriendToolbarNotificationComposer = 3082; // PRODUCTION-201611291003-338511768 + public final static int SimpleAlertComposer = 5100; // PRODUCTION-201611291003-338511768 public final static int MessengerErrorComposer = 896; // PRODUCTION-201611291003-338511768 public final static int CameraPriceComposer = 3878; // PRODUCTION-201611291003-338511768 public final static int PetBreedingCompleted = 2527; // PRODUCTION-201611291003-338511768 @@ -504,6 +512,7 @@ public class Outgoing { public final static int WiredOpenComposer = 1830; public final static int UnknownCatalogPageOfferComposer = 1889; public final static int NuxAlertComposer = 2023; + public final static int InClientLinkComposer = 2023; public final static int HotelViewExpiringCatalogPageCommposer = 2515; public final static int UnknownHabboWayQuizComposer = 2772; public final static int PetLevelUpdatedComposer = 2824; @@ -554,4 +563,24 @@ public class Outgoing { public static final int SnowStormUserRematchedComposer = 5029; + // Furni Editor + public static final int FurniEditorSearchComposer = 10040; + public static final int FurniEditorDetailComposer = 10041; + public static final int FurniEditorInteractionsComposer = 10043; + public static final int FurniEditorResultComposer = 10044; + + // Catalog Admin + public static final int CatalogAdminResultComposer = 10059; + + // Custom Prefixes + public static final int UserPrefixesComposer = 7001; + public static final int PrefixReceivedComposer = 7002; + public static final int ActivePrefixUpdatedComposer = 7003; + public static final int AvailableCommandsComposer = 4050; + + // YouTube Room Broadcast + public static final int YouTubeRoomBroadcastComposer = 8001; + public static final int YouTubeRoomWatchersComposer = 8002; + public static final int YouTubeRoomSettingsComposer = 8003; + } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/BuildersClubFurniCountComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/BuildersClubFurniCountComposer.java new file mode 100644 index 00000000..7861cf8d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/BuildersClubFurniCountComposer.java @@ -0,0 +1,20 @@ +package com.eu.habbo.messages.outgoing.catalog; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class BuildersClubFurniCountComposer extends MessageComposer { + private final int furniCount; + + public BuildersClubFurniCountComposer(int furniCount) { + this.furniCount = furniCount; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.CatalogModeComposer); + this.response.appendInt(this.furniCount); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/BuildersClubSubscriptionStatusComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/BuildersClubSubscriptionStatusComposer.java new file mode 100644 index 00000000..2e11b68f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/BuildersClubSubscriptionStatusComposer.java @@ -0,0 +1,48 @@ +package com.eu.habbo.messages.outgoing.catalog; + +import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class BuildersClubSubscriptionStatusComposer extends MessageComposer { + private final int secondsLeft; + private final int furniLimit; + private final int maxFurniLimit; + private final int secondsLeftWithGrace; + private final boolean placementBlockedByVisitors; + private final boolean placementAllowedInCurrentRoom; + + public BuildersClubSubscriptionStatusComposer(Habbo habbo) { + this( + BuildersClubRoomSupport.getMembershipSecondsLeft(habbo.getHabboInfo().getId()), + BuildersClubRoomSupport.getFurniLimit(habbo.getHabboInfo().getId()), + BuildersClubRoomSupport.getFurniLimit(habbo.getHabboInfo().getId()), + BuildersClubRoomSupport.getMembershipSecondsLeft(habbo.getHabboInfo().getId()), + BuildersClubRoomSupport.isPlacementBlockedByVisitors(habbo), + BuildersClubRoomSupport.canPlaceInCurrentRoom(habbo) + ); + } + + public BuildersClubSubscriptionStatusComposer(int secondsLeft, int furniLimit, int maxFurniLimit, int secondsLeftWithGrace, boolean placementBlockedByVisitors, boolean placementAllowedInCurrentRoom) { + this.secondsLeft = Math.max(0, secondsLeft); + this.furniLimit = Math.max(0, furniLimit); + this.maxFurniLimit = Math.max(0, maxFurniLimit); + this.secondsLeftWithGrace = Math.max(0, secondsLeftWithGrace); + this.placementBlockedByVisitors = placementBlockedByVisitors; + this.placementAllowedInCurrentRoom = placementAllowedInCurrentRoom; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.BuildersClubExpiredComposer); + this.response.appendInt(this.secondsLeft); + this.response.appendInt(this.furniLimit); + this.response.appendInt(this.maxFurniLimit); + this.response.appendInt(this.secondsLeftWithGrace); + this.response.appendBoolean(this.placementBlockedByVisitors); + this.response.appendBoolean(this.placementAllowedInCurrentRoom); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/CatalogPagesListComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/CatalogPagesListComposer.java index 018d441a..c0acdc53 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/CatalogPagesListComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/CatalogPagesListComposer.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.outgoing.catalog; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.catalog.CatalogPage; +import com.eu.habbo.habbohotel.catalog.CatalogPageType; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.ServerMessage; @@ -15,6 +16,10 @@ import java.util.List; public class CatalogPagesListComposer extends MessageComposer { private static final Logger LOGGER = LoggerFactory.getLogger(CatalogPagesListComposer.class); + private static final int MAX_OFFERS = 1000; + private static final int MAX_CHILDREN = 500; + private static final int MAX_DEPTH = 20; + private final Habbo habbo; private final String mode; private final boolean hasPermission; @@ -28,7 +33,8 @@ public class CatalogPagesListComposer extends MessageComposer { @Override protected ServerMessage composeInternal() { try { - List pages = Emulator.getGameEnvironment().getCatalogManager().getCatalogPages(-1, this.habbo); + CatalogPageType requestedType = CatalogPageType.fromString(this.mode); + List pages = Emulator.getGameEnvironment().getCatalogManager().getCatalogPages(-1, this.habbo, requestedType); this.response.init(Outgoing.CatalogPagesListComposer); @@ -38,10 +44,12 @@ public class CatalogPagesListComposer extends MessageComposer { this.response.appendString("root"); this.response.appendString(""); this.response.appendInt(0); - this.response.appendInt(pages.size()); - for (CatalogPage category : pages) { - this.append(category); + int childCount = Math.min(pages.size(), MAX_CHILDREN); + this.response.appendInt(childCount); + + for (int idx = 0; idx < childCount; idx++) { + this.append(pages.get(idx), 1, requestedType); } this.response.appendBoolean(false); @@ -55,24 +63,33 @@ public class CatalogPagesListComposer extends MessageComposer { return null; } - private void append(CatalogPage category) { - List pagesList = Emulator.getGameEnvironment().getCatalogManager().getCatalogPages(category.getId(), this.habbo); + private void append(CatalogPage category, int depth, CatalogPageType requestedType) { + List pagesList = Emulator.getGameEnvironment().getCatalogManager().getCatalogPages(category.getId(), this.habbo, requestedType); this.response.appendBoolean(category.isVisible()); this.response.appendInt(category.getIconImage()); this.response.appendInt(category.isEnabled() ? category.getId() : -1); this.response.appendString(category.getPageName()); this.response.appendString(category.getCaption() + (this.hasPermission ? " (" + category.getId() + ")" : "")); - this.response.appendInt(category.getOfferIds().size()); - for (int i : category.getOfferIds().toArray()) { - this.response.appendInt(i); + int[] offers = category.getOfferIds().toArray(); + int offerCount = Math.min(offers.length, MAX_OFFERS); + this.response.appendInt(offerCount); + + for (int idx = 0; idx < offerCount; idx++) { + this.response.appendInt(offers[idx]); } - this.response.appendInt(pagesList.size()); + if (depth >= MAX_DEPTH) { + this.response.appendInt(0); + return; + } - for (CatalogPage page : pagesList) { - this.append(page); + int childCount = Math.min(pagesList.size(), MAX_CHILDREN); + this.response.appendInt(childCount); + + for (int idx = 0; idx < childCount; idx++) { + this.append(pagesList.get(idx), depth + 1, requestedType); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/ClubDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/ClubDataComposer.java index 6a1ff748..070f9a5c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/ClubDataComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/ClubDataComposer.java @@ -3,6 +3,7 @@ package com.eu.habbo.messages.outgoing.catalog; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.catalog.ClubOffer; import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.subscriptions.Subscription; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.Outgoing; @@ -22,12 +23,15 @@ public class ClubDataComposer extends MessageComposer { protected ServerMessage composeInternal() { this.response.init(Outgoing.ClubDataComposer); - List offers = Emulator.getGameEnvironment().getCatalogManager().getClubOffers(); + List offers = Emulator.getGameEnvironment().getCatalogManager().getClubOffers(this.windowId); this.response.appendInt(offers.size()); - //TODO Change this to a seperate table. for (ClubOffer offer : offers) { - offer.serialize(this.response, this.habbo.getHabboStats().getClubExpireTimestamp()); + int expireTimestamp = offer.isBuildersClubSubscription() + ? this.habbo.getHabboStats().getSubscriptionExpireTimestamp(Subscription.BUILDERS_CLUB) + : (offer.isBuildersClubAddon() ? Emulator.getIntUnixTimestamp() : this.habbo.getHabboStats().getClubExpireTimestamp()); + + offer.serialize(this.response, expireTimestamp); } this.response.appendInt(this.windowId); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/catalogadmin/CatalogAdminResultComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/catalogadmin/CatalogAdminResultComposer.java new file mode 100644 index 00000000..af554141 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/catalog/catalogadmin/CatalogAdminResultComposer.java @@ -0,0 +1,23 @@ +package com.eu.habbo.messages.outgoing.catalog.catalogadmin; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class CatalogAdminResultComposer extends MessageComposer { + private final boolean success; + private final String message; + + public CatalogAdminResultComposer(boolean success, String message) { + this.success = success; + this.message = message; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.CatalogAdminResultComposer); + this.response.appendBoolean(this.success); + this.response.appendString(this.message); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/commands/AvailableCommandsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/commands/AvailableCommandsComposer.java new file mode 100644 index 00000000..0f8e00f5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/commands/AvailableCommandsComposer.java @@ -0,0 +1,32 @@ +package com.eu.habbo.messages.outgoing.commands; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.commands.Command; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.List; + +public class AvailableCommandsComposer extends MessageComposer { + private final List commands; + + public AvailableCommandsComposer(List commands) { + this.commands = commands; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.AvailableCommandsComposer); + this.response.appendInt(this.commands.size()); + + for (Command cmd : this.commands) { + this.response.appendString(cmd.keys[0]); + this.response.appendString( + Emulator.getTexts().getValue("commands.description." + cmd.permission, cmd.permission) + ); + } + + return this.response; + } +} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorDetailComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorDetailComposer.java new file mode 100644 index 00000000..7c351af8 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorDetailComposer.java @@ -0,0 +1,77 @@ +package com.eu.habbo.messages.outgoing.furnieditor; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.List; +import java.util.Map; + +public class FurniEditorDetailComposer extends MessageComposer { + private final Map item; + private final int usageCount; + private final List> catalogItems; + private final String furniDataJson; + + public FurniEditorDetailComposer(Map item, int usageCount, List> catalogItems, String furniDataJson) { + this.item = item; + this.usageCount = usageCount; + this.catalogItems = catalogItems; + this.furniDataJson = furniDataJson; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.FurniEditorDetailComposer); + + // 14 base fields + this.response.appendInt((int) item.get("id")); + this.response.appendInt((int) item.get("sprite_id")); + this.response.appendString((String) item.getOrDefault("item_name", "")); + this.response.appendString((String) item.getOrDefault("public_name", "")); + this.response.appendString((String) item.getOrDefault("type", "s")); + this.response.appendInt((int) item.getOrDefault("width", 1)); + this.response.appendInt((int) item.getOrDefault("length", 1)); + this.response.appendDouble((double) item.getOrDefault("stack_height", 0.0)); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_stack", "1")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_walk", "0")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_sit", "0")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_lay", "0")))); + this.response.appendString((String) item.getOrDefault("interaction_type", "")); + this.response.appendInt((int) item.getOrDefault("interaction_modes_count", 0)); + + // 13 extended fields + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_gift", "1")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_trade", "1")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_recycle", "1")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_marketplace_sell", "1")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_inventory_stack", "1")))); + this.response.appendString((String) item.getOrDefault("vending_ids", "")); + this.response.appendString((String) item.getOrDefault("customparams", "")); + this.response.appendInt((int) item.getOrDefault("effect_id_male", 0)); + this.response.appendInt((int) item.getOrDefault("effect_id_female", 0)); + this.response.appendString((String) item.getOrDefault("clothing_on_walk", "")); + this.response.appendString((String) item.getOrDefault("multiheight", "")); + this.response.appendString((String) item.getOrDefault("description", "")); + + // usage count + this.response.appendInt(this.usageCount); + + // catalog references + this.response.appendInt(this.catalogItems.size()); + for (Map ci : this.catalogItems) { + this.response.appendInt((int) ci.get("id")); + this.response.appendString((String) ci.getOrDefault("catalog_name", "")); + this.response.appendInt((int) ci.getOrDefault("cost_credits", 0)); + this.response.appendInt((int) ci.getOrDefault("cost_points", 0)); + this.response.appendInt((int) ci.getOrDefault("points_type", 0)); + this.response.appendInt((int) ci.getOrDefault("page_id", -1)); + this.response.appendString((String) ci.getOrDefault("page_caption", "")); + } + + // furnidata JSON string + this.response.appendString(this.furniDataJson != null ? this.furniDataJson : "{}"); + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorInteractionsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorInteractionsComposer.java new file mode 100644 index 00000000..ea778e65 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorInteractionsComposer.java @@ -0,0 +1,27 @@ +package com.eu.habbo.messages.outgoing.furnieditor; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.List; + +public class FurniEditorInteractionsComposer extends MessageComposer { + private final List interactions; + + public FurniEditorInteractionsComposer(List interactions) { + this.interactions = interactions; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.FurniEditorInteractionsComposer); + this.response.appendInt(this.interactions.size()); + + for (String interaction : this.interactions) { + this.response.appendString(interaction); + } + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorResultComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorResultComposer.java new file mode 100644 index 00000000..07bc8e67 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorResultComposer.java @@ -0,0 +1,30 @@ +package com.eu.habbo.messages.outgoing.furnieditor; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class FurniEditorResultComposer extends MessageComposer { + private final boolean success; + private final String message; + private final int id; + + public FurniEditorResultComposer(boolean success, String message) { + this(success, message, -1); + } + + public FurniEditorResultComposer(boolean success, String message, int id) { + this.success = success; + this.message = message; + this.id = id; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.FurniEditorResultComposer); + this.response.appendBoolean(this.success); + this.response.appendString(this.message); + this.response.appendInt(this.id); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorSearchComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorSearchComposer.java new file mode 100644 index 00000000..c7ec1fcb --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/furnieditor/FurniEditorSearchComposer.java @@ -0,0 +1,50 @@ +package com.eu.habbo.messages.outgoing.furnieditor; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +public class FurniEditorSearchComposer extends MessageComposer { + private final List> items; + private final int total; + private final int page; + + public FurniEditorSearchComposer(List> items, int total, int page) { + this.items = items; + this.total = total; + this.page = page; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.FurniEditorSearchComposer); + this.response.appendInt(this.items.size()); + + for (Map item : this.items) { + this.response.appendInt((int) item.get("id")); + this.response.appendInt((int) item.get("sprite_id")); + this.response.appendString((String) item.getOrDefault("item_name", "")); + this.response.appendString((String) item.getOrDefault("public_name", "")); + this.response.appendString((String) item.getOrDefault("type", "s")); + this.response.appendInt((int) item.getOrDefault("width", 1)); + this.response.appendInt((int) item.getOrDefault("length", 1)); + this.response.appendDouble((double) item.getOrDefault("stack_height", 0.0)); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_stack", "1")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_walk", "0")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_sit", "0")))); + this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_lay", "0")))); + this.response.appendString((String) item.getOrDefault("interaction_type", "")); + this.response.appendInt((int) item.getOrDefault("interaction_modes_count", 0)); + } + + this.response.appendInt(this.total); + this.response.appendInt(this.page); + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/generic/alerts/SimpleAlertComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/generic/alerts/SimpleAlertComposer.java new file mode 100644 index 00000000..c8a7c8c7 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/generic/alerts/SimpleAlertComposer.java @@ -0,0 +1,31 @@ +package com.eu.habbo.messages.outgoing.generic.alerts; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class SimpleAlertComposer extends MessageComposer { + private final String alertMessage; + private final String titleMessage; + + public SimpleAlertComposer(String alertMessage) { + this(alertMessage, null); + } + + public SimpleAlertComposer(String alertMessage, String titleMessage) { + this.alertMessage = alertMessage; + this.titleMessage = titleMessage; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.SimpleAlertComposer); + this.response.appendString(this.alertMessage); + + if (this.titleMessage != null && !this.titleMessage.isEmpty()) { + this.response.appendString(this.titleMessage); + } + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/GuildManageComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/GuildManageComposer.java index 6c22192f..22619f34 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/GuildManageComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/GuildManageComposer.java @@ -62,6 +62,7 @@ public class GuildManageComposer extends MessageComposer { } this.response.appendString(this.guild.getBadge()); this.response.appendInt(this.guild.getMemberCount()); + this.response.appendBoolean(this.guild.hasForum()); return this.response; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/forums/GuildForumDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/forums/GuildForumDataComposer.java index d753f9a3..64abf8c3 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/forums/GuildForumDataComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/forums/GuildForumDataComposer.java @@ -20,11 +20,20 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.concurrent.ConcurrentHashMap; public class GuildForumDataComposer extends MessageComposer { private static final Logger LOGGER = LoggerFactory.getLogger(GuildForumDataComposer.class); + // Cache for user last-seen timestamps: key = "userId:guildId", value = {timestamp, cachedAt} + private static final ConcurrentHashMap lastSeenCache = new ConcurrentHashMap<>(); + private static final long LAST_SEEN_CACHE_TTL = 5 * 60 * 1000; // 5 minutes + + // Cache for unread counts: key = "guildId:lastSeenAt", value = {count, cachedAt} + private static final ConcurrentHashMap unreadCache = new ConcurrentHashMap<>(); + private static final long UNREAD_CACHE_TTL = 2 * 60 * 1000; // 2 minutes + public final Guild guild; public Habbo habbo; @@ -33,13 +42,77 @@ public class GuildForumDataComposer extends MessageComposer { this.habbo = habbo; } + public static void invalidateLastSeenCache(int userId, int guildId) { + lastSeenCache.remove(userId + ":" + guildId); + } + + public static void invalidateUnreadCache(int guildId) { + unreadCache.entrySet().removeIf(entry -> entry.getKey().startsWith(guildId + ":")); + } + + private static int getLastSeenAt(int userId, int guildId) { + String key = userId + ":" + guildId; + long now = System.currentTimeMillis(); + + long[] cached = lastSeenCache.get(key); + if (cached != null && (now - cached[1]) < LAST_SEEN_CACHE_TTL) { + return (int) cached[0]; + } + + int lastSeenAt = 0; + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( + "SELECT `timestamp` FROM `guild_forum_views` WHERE `user_id` = ? AND `guild_id` = ? LIMIT 1" + )) { + statement.setInt(1, userId); + statement.setInt(2, guildId); + ResultSet set = statement.executeQuery(); + if (set.next()) { + lastSeenAt = set.getInt("timestamp"); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + + lastSeenCache.put(key, new long[]{lastSeenAt, now}); + return lastSeenAt; + } + + private static int getUnreadCount(int guildId, int lastSeenAt) { + String key = guildId + ":" + lastSeenAt; + long now = System.currentTimeMillis(); + + long[] cached = unreadCache.get(key); + if (cached != null && (now - cached[1]) < UNREAD_CACHE_TTL) { + return (int) cached[0]; + } + + int newComments = 0; + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( + "SELECT COUNT(*) FROM `guilds_forums_comments` " + + "JOIN `guilds_forums_threads` ON `guilds_forums_threads`.`id` = `guilds_forums_comments`.`thread_id` " + + "WHERE `guilds_forums_threads`.`guild_id` = ? AND `guilds_forums_comments`.`created_at` > ?" + )) { + statement.setInt(1, guildId); + statement.setInt(2, lastSeenAt); + + ResultSet set = statement.executeQuery(); + if (set.next()) { + newComments = set.getInt(1); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + + unreadCache.put(key, new long[]{newComments, now}); + return newComments; + } + public static void serializeForumData(ServerMessage response, Guild guild, Habbo habbo) { final THashSet forumThreads = ForumThread.getByGuildId(guild.getId()); - int lastSeenAt = 0; + int lastSeenAt = getLastSeenAt(habbo.getHabboInfo().getId(), guild.getId()); int totalComments = 0; - int newComments = 0; int totalThreads = 0; ForumThreadComment lastComment = null; @@ -55,31 +128,7 @@ public class GuildForumDataComposer extends MessageComposer { } } - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "SELECT COUNT(*) " + - "FROM guilds_forums_threads A " + - "JOIN ( " + - "SELECT * " + - "FROM `guilds_forums_comments` " + - "WHERE `id` IN ( " + - "SELECT id " + - "FROM `guilds_forums_comments` B " + - "ORDER BY B.`id` ASC " + - ") " + - "ORDER BY `id` DESC " + - ") B ON A.`id` = B.`thread_id` " + - "WHERE A.`guild_id` = ? AND B.`created_at` > ?" - )) { - statement.setInt(1, guild.getId()); - statement.setInt(2, lastSeenAt); - - ResultSet set = statement.executeQuery(); - while (set.next()) { - newComments = set.getInt(1); - } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } + int newComments = getUnreadCount(guild.getId(), lastSeenAt); response.appendInt(guild.getId()); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/ActivePrefixUpdatedComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/ActivePrefixUpdatedComposer.java new file mode 100644 index 00000000..13017e93 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/ActivePrefixUpdatedComposer.java @@ -0,0 +1,35 @@ +package com.eu.habbo.messages.outgoing.inventory.prefixes; + +import com.eu.habbo.habbohotel.users.UserPrefix; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class ActivePrefixUpdatedComposer extends MessageComposer { + private final UserPrefix prefix; + + public ActivePrefixUpdatedComposer(UserPrefix prefix) { + this.prefix = prefix; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.ActivePrefixUpdatedComposer); + + if (this.prefix != null) { + this.response.appendInt(this.prefix.getId()); + this.response.appendString(this.prefix.getText()); + this.response.appendString(this.prefix.getColor()); + this.response.appendString(this.prefix.getIcon()); + this.response.appendString(this.prefix.getEffect()); + } else { + this.response.appendInt(0); + this.response.appendString(""); + this.response.appendString(""); + this.response.appendString(""); + this.response.appendString(""); + } + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/PrefixReceivedComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/PrefixReceivedComposer.java new file mode 100644 index 00000000..98bdf055 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/PrefixReceivedComposer.java @@ -0,0 +1,25 @@ +package com.eu.habbo.messages.outgoing.inventory.prefixes; + +import com.eu.habbo.habbohotel.users.UserPrefix; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class PrefixReceivedComposer extends MessageComposer { + private final UserPrefix prefix; + + public PrefixReceivedComposer(UserPrefix prefix) { + this.prefix = prefix; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.PrefixReceivedComposer); + this.response.appendInt(this.prefix.getId()); + this.response.appendString(this.prefix.getText()); + this.response.appendString(this.prefix.getColor()); + this.response.appendString(this.prefix.getIcon()); + this.response.appendString(this.prefix.getEffect()); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/UserPrefixesComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/UserPrefixesComposer.java new file mode 100644 index 00000000..747e63b6 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/inventory/prefixes/UserPrefixesComposer.java @@ -0,0 +1,38 @@ +package com.eu.habbo.messages.outgoing.inventory.prefixes; + +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.UserPrefix; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.List; + +public class UserPrefixesComposer extends MessageComposer { + private final Habbo habbo; + + public UserPrefixesComposer(Habbo habbo) { + this.habbo = habbo; + } + + @Override + protected ServerMessage composeInternal() { + if (this.habbo == null) return null; + + List prefixes = this.habbo.getInventory().getPrefixesComponent().getPrefixes(); + + this.response.init(Outgoing.UserPrefixesComposer); + this.response.appendInt(prefixes.size()); + + for (UserPrefix prefix : prefixes) { + this.response.appendInt(prefix.getId()); + this.response.appendString(prefix.getText()); + this.response.appendString(prefix.getColor()); + this.response.appendString(prefix.getIcon()); + this.response.appendString(prefix.getEffect()); + this.response.appendInt(prefix.isActive() ? 1 : 0); + } + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolUserInfoComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolUserInfoComposer.java index 5eba2421..4772d455 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolUserInfoComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolUserInfoComposer.java @@ -18,9 +18,11 @@ public class ModToolUserInfoComposer extends MessageComposer { private static final Logger LOGGER = LoggerFactory.getLogger(ModToolUserInfoComposer.class); private final ResultSet set; + private final boolean hideMail; - public ModToolUserInfoComposer(ResultSet set) { + public ModToolUserInfoComposer(ResultSet set, boolean hideMail) { this.set = set; + this.hideMail = hideMail; } @Override @@ -58,7 +60,7 @@ public class ModToolUserInfoComposer extends MessageComposer { this.response.appendString(""); //Last Purchase Timestamp this.response.appendInt(this.set.getInt("user_id")); //Personal Identification # this.response.appendInt(0); // Number of account bans - this.response.appendString(this.set.getBoolean("hide_mail") ? "" : this.set.getString("mail")); + this.response.appendString(this.hideMail ? "" : this.set.getString("mail")); this.response.appendString("Rank (" + this.set.getInt("rank_id") + "): " + this.set.getString("rank_name")); //user_class_txt ModToolSanctions modToolSanctions = Emulator.getGameEnvironment().getModToolSanctions(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomDataComposer.java index 01326eac..50770114 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomDataComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomDataComposer.java @@ -3,10 +3,12 @@ package com.eu.habbo.messages.outgoing.rooms; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.guilds.Guild; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomPromotion; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.Outgoing; +import com.eu.habbo.util.HotelDateTimeUtil; public class RoomDataComposer extends MessageComposer { private final Room room; @@ -23,6 +25,9 @@ public class RoomDataComposer extends MessageComposer { @Override protected ServerMessage composeInternal() { + final RoomPromotion promotion = this.room.getPromotion(); + final boolean hasPromotion = this.room.isPromoted() && (promotion != null); + this.response.init(Outgoing.RoomDataComposer); this.response.appendBoolean(this.enterRoom); this.response.appendInt(this.room.getId()); @@ -63,7 +68,7 @@ public class RoomDataComposer extends MessageComposer { base = base | 8; } - if (this.room.isPromoted()) { + if (hasPromotion) { base = base | 4; } @@ -86,10 +91,10 @@ public class RoomDataComposer extends MessageComposer { } } - if (this.room.isPromoted()) { - this.response.appendString(this.room.getPromotion().getTitle()); - this.response.appendString(this.room.getPromotion().getDescription()); - this.response.appendInt((this.room.getPromotion().getEndTimestamp() - Emulator.getIntUnixTimestamp()) / 60); + if (hasPromotion) { + this.response.appendString(promotion.getTitle()); + this.response.appendString(promotion.getDescription()); + this.response.appendInt((promotion.getEndTimestamp() - Emulator.getIntUnixTimestamp()) / 60); } this.response.appendBoolean(this.roomForward); @@ -108,6 +113,9 @@ public class RoomDataComposer extends MessageComposer { this.response.appendInt(this.room.getChatSpeed()); this.response.appendInt(this.room.getChatDistance()); this.response.appendInt(this.room.getChatProtection()); + this.response.appendString(HotelDateTimeUtil.getTimezoneId()); + this.response.appendString(String.valueOf(HotelDateTimeUtil.now().toInstant().toEpochMilli())); + this.response.appendInt(Room.MAXIMUM_FURNI); return this.response; @@ -128,4 +136,4 @@ public class RoomDataComposer extends MessageComposer { public boolean isEnterRoom() { return enterRoom; } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomRightsListComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomRightsListComposer.java index 42e2c571..f9626bc1 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomRightsListComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomRightsListComposer.java @@ -4,7 +4,6 @@ import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.Outgoing; -import gnu.trove.map.hash.THashMap; import java.util.Map; @@ -20,7 +19,7 @@ public class RoomRightsListComposer extends MessageComposer { this.response.init(Outgoing.RoomRightsListComposer); this.response.appendInt(this.room.getId()); - THashMap rightsMap = this.room.getUsersWithRights(); + Map rightsMap = this.room.getUsersWithRights(); this.response.appendInt(rightsMap.size()); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/WiredMovementsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/WiredMovementsComposer.java new file mode 100644 index 00000000..d95cad7c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/WiredMovementsComposer.java @@ -0,0 +1,330 @@ +package com.eu.habbo.messages.outgoing.rooms; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.UUID; + +public class WiredMovementsComposer extends MessageComposer { + public static final int TYPE_USER_MOVE = 0; + public static final int TYPE_FURNI_MOVE = 1; + public static final int TYPE_WALL_ITEM_MOVE = 2; + public static final int TYPE_USER_DIRECTION = 3; + + public static final int FURNI_ANCHOR_NONE = 0; + public static final int FURNI_ANCHOR_USER = 1; + public static final int FURNI_ANCHOR_FURNI = 2; + + public static final int USER_MOVEMENT_WALK = 0; + public static final int USER_MOVEMENT_SLIDE = 1; + public static final int DEFAULT_DURATION = 500; + + private final List movements; + + public WiredMovementsComposer(List movements) { + this.movements = normalizeMovements(movements == null ? new ArrayList<>() : movements); + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.WiredMovementsComposer); + this.response.appendInt(this.movements.size()); + + for (MovementData movement : this.movements) { + this.response.appendInt(movement.getType()); + movement.append(this.response); + } + + return this.response; + } + + private static List normalizeMovements(List source) + { + if((source == null) || source.isEmpty()) return new ArrayList<>(); + + final LinkedHashMap normalized = new LinkedHashMap<>(); + + for(final MovementData movement : source) + { + if(movement == null) continue; + + final String key = movementKey(movement); + + if(key == null) + { + normalized.put(UUID.randomUUID().toString(), movement); + continue; + } + + final MovementData existing = normalized.get(key); + + if(existing == null) + { + normalized.put(key, movement); + continue; + } + + normalized.put(key, mergeMovement(existing, movement)); + } + + return new ArrayList<>(normalized.values()); + } + + private static String movementKey(MovementData movement) + { + if(movement instanceof FurniMovementData) + { + return "furni:" + ((FurniMovementData) movement).id; + } + + if(movement instanceof UserMovementData) + { + return "user:" + ((UserMovementData) movement).id; + } + + if(movement instanceof UserDirectionData) + { + return "userdir:" + ((UserDirectionData) movement).id; + } + + if(movement instanceof WallItemMovementData) + { + return "wall:" + ((WallItemMovementData) movement).id; + } + + return null; + } + + private static MovementData mergeMovement(MovementData previous, MovementData current) + { + if((previous instanceof FurniMovementData) && (current instanceof FurniMovementData)) + { + final FurniMovementData oldMovement = (FurniMovementData) previous; + final FurniMovementData newMovement = (FurniMovementData) current; + + return furniMovement( + oldMovement.id, + oldMovement.fromX, + oldMovement.fromY, + newMovement.toX, + newMovement.toY, + oldMovement.fromZ, + newMovement.toZ, + newMovement.rotation, + newMovement.duration, + newMovement.elapsed, + newMovement.anchorType, + newMovement.anchorId); + } + + if((previous instanceof UserMovementData) && (current instanceof UserMovementData)) + { + final UserMovementData oldMovement = (UserMovementData) previous; + final UserMovementData newMovement = (UserMovementData) current; + + return new UserMovementData( + oldMovement.id, + oldMovement.fromX, + oldMovement.fromY, + newMovement.toX, + newMovement.toY, + oldMovement.fromZ, + newMovement.toZ, + newMovement.movementType, + newMovement.bodyDirection, + newMovement.headDirection, + newMovement.duration); + } + + return current; + } + + public static MovementData furniMovement(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ) { + return furniMovement(id, fromX, fromY, toX, toY, fromZ, toZ, 0, DEFAULT_DURATION, 0, FURNI_ANCHOR_NONE, 0); + } + + public static MovementData furniMovement(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ, int rotation, int duration) { + return furniMovement(id, fromX, fromY, toX, toY, fromZ, toZ, rotation, duration, 0, FURNI_ANCHOR_NONE, 0); + } + + public static MovementData furniMovement(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ, int rotation, int duration, int elapsed) { + return furniMovement(id, fromX, fromY, toX, toY, fromZ, toZ, rotation, duration, elapsed, FURNI_ANCHOR_NONE, 0); + } + + public static MovementData furniMovement(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ, int rotation, int duration, int elapsed, int anchorType, int anchorId) { + return new FurniMovementData(id, fromX, fromY, toX, toY, fromZ, toZ, rotation, duration, elapsed, anchorType, anchorId); + } + + public static MovementData userWalkMovement(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ, int bodyDirection, int headDirection, int duration) { + return new UserMovementData(id, fromX, fromY, toX, toY, fromZ, toZ, USER_MOVEMENT_WALK, bodyDirection, headDirection, duration); + } + + public static MovementData userSlideMovement(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ, int bodyDirection, int headDirection, int duration) { + return new UserMovementData(id, fromX, fromY, toX, toY, fromZ, toZ, USER_MOVEMENT_SLIDE, bodyDirection, headDirection, duration); + } + + public static MovementData userDirectionUpdate(int id, int headDirection, int bodyDirection) { + return new UserDirectionData(id, headDirection, bodyDirection); + } + + public static MovementData wallItemMovement(int id, boolean enabled, int[] values) { + return new WallItemMovementData(id, enabled, values); + } + + public interface MovementData { + int getType(); + + void append(ServerMessage response); + } + + private abstract static class BaseMovementData implements MovementData { + private final int type; + + private BaseMovementData(int type) { + this.type = type; + } + + @Override + public int getType() { + return this.type; + } + } + + private static final class UserMovementData extends BaseMovementData { + private final int id; + private final int fromX; + private final int fromY; + private final int toX; + private final int toY; + private final double fromZ; + private final double toZ; + private final int movementType; + private final int bodyDirection; + private final int headDirection; + private final int duration; + + private UserMovementData(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ, int movementType, int bodyDirection, int headDirection, int duration) { + super(TYPE_USER_MOVE); + this.id = id; + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + this.fromZ = fromZ; + this.toZ = toZ; + this.movementType = movementType; + this.bodyDirection = bodyDirection; + this.headDirection = headDirection; + this.duration = duration; + } + + @Override + public void append(ServerMessage response) { + response.appendInt(this.fromX); + response.appendInt(this.fromY); + response.appendInt(this.toX); + response.appendInt(this.toY); + response.appendString(Double.toString(this.fromZ)); + response.appendString(Double.toString(this.toZ)); + response.appendInt(this.id); + response.appendInt(this.movementType); + response.appendInt(this.bodyDirection); + response.appendInt(this.headDirection); + response.appendInt(this.duration); + } + } + + private static final class FurniMovementData extends BaseMovementData { + private final int id; + private final int fromX; + private final int fromY; + private final int toX; + private final int toY; + private final double fromZ; + private final double toZ; + private final int rotation; + private final int duration; + private final int elapsed; + private final int anchorType; + private final int anchorId; + + private FurniMovementData(int id, int fromX, int fromY, int toX, int toY, double fromZ, double toZ, int rotation, int duration, int elapsed, int anchorType, int anchorId) { + super(TYPE_FURNI_MOVE); + this.id = id; + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + this.fromZ = fromZ; + this.toZ = toZ; + this.rotation = rotation; + this.duration = duration; + this.elapsed = elapsed; + this.anchorType = anchorType; + this.anchorId = anchorId; + } + + @Override + public void append(ServerMessage response) { + response.appendInt(this.fromX); + response.appendInt(this.fromY); + response.appendInt(this.toX); + response.appendInt(this.toY); + response.appendString(Double.toString(this.fromZ)); + response.appendString(Double.toString(this.toZ)); + response.appendInt(this.id); + response.appendInt(this.rotation); + response.appendInt(this.duration); + response.appendInt(this.elapsed); + response.appendInt(this.anchorType); + response.appendInt(this.anchorId); + } + } + + private static final class UserDirectionData extends BaseMovementData { + private final int id; + private final int headDirection; + private final int bodyDirection; + + private UserDirectionData(int id, int headDirection, int bodyDirection) { + super(TYPE_USER_DIRECTION); + this.id = id; + this.headDirection = headDirection; + this.bodyDirection = bodyDirection; + } + + @Override + public void append(ServerMessage response) { + response.appendInt(this.id); + response.appendInt(this.headDirection); + response.appendInt(this.bodyDirection); + } + } + + private static final class WallItemMovementData extends BaseMovementData { + private final int id; + private final boolean enabled; + private final int[] values; + + private WallItemMovementData(int id, boolean enabled, int[] values) { + super(TYPE_WALL_ITEM_MOVE); + this.id = id; + this.enabled = enabled; + this.values = values == null ? new int[9] : values; + } + + @Override + public void append(ServerMessage response) { + response.appendInt(this.id); + response.appendBoolean(this.enabled); + + for (int index = 0; index < 9; index++) { + response.appendInt(index < this.values.length ? this.values[index] : 0); + } + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AddFloorItemComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AddFloorItemComposer.java index 809fede9..94c29a61 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AddFloorItemComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AddFloorItemComposer.java @@ -19,11 +19,24 @@ public class AddFloorItemComposer extends MessageComposer { protected ServerMessage composeInternal() { this.response.init(Outgoing.AddFloorItemComposer); this.item.serializeFloorData(this.response); - this.response.appendInt(this.item instanceof InteractionGift ? ((((InteractionGift) this.item).getColorId() * 1000) + ((InteractionGift) this.item).getRibbonId()) : (this.item instanceof InteractionMusicDisc ? ((InteractionMusicDisc) this.item).getSongId() : 1)); + this.response.appendInt( + this.item instanceof InteractionGift + ? ((((InteractionGift) this.item).getColorId() * 1000) + ((InteractionGift) this.item).getRibbonId()) + : (this.item instanceof InteractionMusicDisc + ? ((InteractionMusicDisc) this.item).getSongId() + : (this.item instanceof InteractionStackWalkHelper ? 2147483001 : 1)) + ); this.item.serializeExtradata(this.response); this.response.appendInt(-1); this.response.appendInt(this.item instanceof InteractionTeleport || this.item instanceof InteractionSwitch || this.item instanceof InteractionSwitchRemoteControl || this.item instanceof InteractionVendingMachine || this.item instanceof InteractionInformationTerminal || this.item instanceof InteractionPostIt|| this.item instanceof InteractionPuzzleBox ? 2 : this.item.isUsable() ? 1 : 0); this.response.appendInt(this.item.getUserId()); + this.response.appendInt(this.item.getBaseItem().allowStack() ? 1 : 0); + this.response.appendInt(this.item.getBaseItem().allowSit() ? 1 : 0); + this.response.appendInt(this.item.getBaseItem().allowLay() ? 1 : 0); + this.response.appendInt(this.item.getBaseItem().allowWalk() ? 1 : 0); + this.response.appendInt(this.item.getBaseItem().getWidth()); + this.response.appendInt(this.item.getBaseItem().getLength()); + this.response.appendInt(this.item.getTeleportTargetId()); this.response.appendString(this.itemOwnerName); return this.response; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AreaHideComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AreaHideComposer.java new file mode 100644 index 00000000..c7e48e55 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/AreaHideComposer.java @@ -0,0 +1,30 @@ +package com.eu.habbo.messages.outgoing.rooms.items; + +import com.eu.habbo.habbohotel.rooms.RoomAreaHideSupport; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class AreaHideComposer extends MessageComposer { + private final HabboItem item; + + public AreaHideComposer(HabboItem item) { + this.item = item; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.AreaHideComposer); + this.response.appendInt(this.item.getId()); + this.response.appendBoolean(RoomAreaHideSupport.isControllerActive(this.item)); + this.response.appendInt(RoomAreaHideSupport.getRootX(this.item)); + this.response.appendInt(RoomAreaHideSupport.getRootY(this.item)); + this.response.appendInt(RoomAreaHideSupport.getWidth(this.item)); + this.response.appendInt(RoomAreaHideSupport.getLength(this.item)); + this.response.appendBoolean(RoomAreaHideSupport.isInverted(this.item)); + this.response.appendBoolean(RoomAreaHideSupport.includesWallItems(this.item)); + this.response.appendBoolean(RoomAreaHideSupport.isInvisibilityEnabled(this.item)); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/ConfInvisStateComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/ConfInvisStateComposer.java new file mode 100644 index 00000000..21ef034b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/ConfInvisStateComposer.java @@ -0,0 +1,34 @@ +package com.eu.habbo.messages.outgoing.rooms.items; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomConfInvisSupport; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; +import gnu.trove.list.array.TIntArrayList; + +public class ConfInvisStateComposer extends MessageComposer { + private final int roomId; + private final boolean active; + private final TIntArrayList hiddenItemIds; + + public ConfInvisStateComposer(Room room) { + this.roomId = (room != null) ? room.getId() : 0; + this.active = RoomConfInvisSupport.hasActiveController(room); + this.hiddenItemIds = RoomConfInvisSupport.collectHiddenFloorItemIds(room); + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.ConfInvisStateComposer); + this.response.appendInt(this.roomId); + this.response.appendBoolean(this.active); + this.response.appendInt(this.hiddenItemIds.size()); + + for (int i = 0; i < this.hiddenItemIds.size(); i++) { + this.response.appendInt(this.hiddenItemIds.get(i)); + } + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/FloorItemUpdateComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/FloorItemUpdateComposer.java index 5ce45b95..88b4dd64 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/FloorItemUpdateComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/FloorItemUpdateComposer.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.outgoing.rooms.items; import com.eu.habbo.habbohotel.items.interactions.InteractionGift; import com.eu.habbo.habbohotel.items.interactions.InteractionMusicDisc; +import com.eu.habbo.habbohotel.items.interactions.InteractionStackWalkHelper; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.ServerMessage; import com.eu.habbo.messages.outgoing.MessageComposer; @@ -18,11 +19,24 @@ public class FloorItemUpdateComposer extends MessageComposer { protected ServerMessage composeInternal() { this.response.init(Outgoing.FloorItemUpdateComposer); this.item.serializeFloorData(this.response); - this.response.appendInt(this.item instanceof InteractionGift ? ((((InteractionGift) this.item).getColorId() * 1000) + ((InteractionGift) this.item).getRibbonId()) : (this.item instanceof InteractionMusicDisc ? ((InteractionMusicDisc) this.item).getSongId() : item.isUsable() ? 0 : 0)); + this.response.appendInt( + this.item instanceof InteractionGift + ? ((((InteractionGift) this.item).getColorId() * 1000) + ((InteractionGift) this.item).getRibbonId()) + : (this.item instanceof InteractionMusicDisc + ? ((InteractionMusicDisc) this.item).getSongId() + : (this.item instanceof InteractionStackWalkHelper ? 2147483001 : 0)) + ); this.item.serializeExtradata(this.response); this.response.appendInt(-1); this.response.appendInt(0); this.response.appendInt(this.item.getUserId()); + this.response.appendInt(this.item.getBaseItem().allowStack() ? 1 : 0); + this.response.appendInt(this.item.getBaseItem().allowSit() ? 1 : 0); + this.response.appendInt(this.item.getBaseItem().allowLay() ? 1 : 0); + this.response.appendInt(this.item.getBaseItem().allowWalk() ? 1 : 0); + this.response.appendInt(this.item.getBaseItem().getWidth()); + this.response.appendInt(this.item.getBaseItem().getLength()); + this.response.appendInt(this.item.getTeleportTargetId()); return this.response; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/HanditemBlockStateComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/HanditemBlockStateComposer.java new file mode 100644 index 00000000..80fa7dce --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/HanditemBlockStateComposer.java @@ -0,0 +1,25 @@ +package com.eu.habbo.messages.outgoing.rooms.items; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomHanditemBlockSupport; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class HanditemBlockStateComposer extends MessageComposer { + private final int roomId; + private final boolean blocked; + + public HanditemBlockStateComposer(Room room) { + this.roomId = (room != null) ? room.getId() : 0; + this.blocked = RoomHanditemBlockSupport.isHanditemBlocked(room); + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.HanditemBlockStateComposer); + this.response.appendInt(this.roomId); + this.response.appendBoolean(this.blocked); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/RoomFloorItemsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/RoomFloorItemsComposer.java index 908d81a3..2045879c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/RoomFloorItemsComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/items/RoomFloorItemsComposer.java @@ -41,11 +41,24 @@ public class RoomFloorItemsComposer extends MessageComposer { for (HabboItem item : this.items) { item.serializeFloorData(this.response); - this.response.appendInt(item instanceof InteractionGift ? ((((InteractionGift) item).getColorId() * 1000) + ((InteractionGift) item).getRibbonId()) : (item instanceof InteractionMusicDisc ? ((InteractionMusicDisc) item).getSongId() : 1)); + this.response.appendInt( + item instanceof InteractionGift + ? ((((InteractionGift) item).getColorId() * 1000) + ((InteractionGift) item).getRibbonId()) + : (item instanceof InteractionMusicDisc + ? ((InteractionMusicDisc) item).getSongId() + : (item instanceof InteractionStackWalkHelper ? 2147483001 : 1)) + ); item.serializeExtradata(this.response); this.response.appendInt(-1); this.response.appendInt(item instanceof InteractionTeleport || item instanceof InteractionSwitch || item instanceof InteractionSwitchRemoteControl || item instanceof InteractionVendingMachine || item instanceof InteractionInformationTerminal || item instanceof InteractionPostIt || item instanceof InteractionPuzzleBox ? 2 : item.isUsable() ? 1 : 0); this.response.appendInt(item.getUserId()); + this.response.appendInt(item.getBaseItem().allowStack() ? 1 : 0); + this.response.appendInt(item.getBaseItem().allowSit() ? 1 : 0); + this.response.appendInt(item.getBaseItem().allowLay() ? 1 : 0); + this.response.appendInt(item.getBaseItem().allowWalk() ? 1 : 0); + this.response.appendInt(item.getBaseItem().getWidth()); + this.response.appendInt(item.getBaseItem().getLength()); + this.response.appendInt(item.getTeleportTargetId()); } return this.response; } 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 00b03985..2820706a 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 @@ -59,6 +59,8 @@ public class RoomPetComposer extends MessageComposer implements TIntObjectProced this.response.appendBoolean(pet instanceof MonsterplantPet && ((MonsterplantPet) pet).isPubliclyBreedable()); //Breedable checkbox //Toggle breeding permission this.response.appendInt(pet instanceof MonsterplantPet ? ((MonsterplantPet) pet).getGrowthStage() : pet.getLevel()); this.response.appendString(""); + this.response.appendString("unknown"); + this.response.appendInt(0); return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUnitOnRollerComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUnitOnRollerComposer.java index 0628a478..4a356e7a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUnitOnRollerComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUnitOnRollerComposer.java @@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionRoller; import com.eu.habbo.habbohotel.pets.Pet; import com.eu.habbo.habbohotel.pets.RideablePet; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomQueueSpeedControlSupport; import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.rooms.RoomUnitType; @@ -157,7 +158,7 @@ public class RoomUnitOnRollerComposer extends MessageComposer { } } } - }, this.room.getRollerSpeed() == 0 ? 250 : InteractionRoller.DELAY); + }, RoomQueueSpeedControlSupport.getEffectiveRollerIntervalMs(this.room)); /* RoomTile rollerTile = room.getLayout().getTile(this.roller.getX(), this.roller.getY()); Emulator.getThreading().run(() -> { @@ -177,7 +178,7 @@ public class RoomUnitOnRollerComposer extends MessageComposer { } } } - }, this.room.getRollerSpeed() == 0 ? 250 : InteractionRoller.DELAY); + }, RoomQueueSpeedControlSupport.getEffectiveRollerIntervalMs(this.room)); */ } else { this.roomUnit.setLocation(this.newLocation); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserStatusComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserStatusComposer.java index 1a6d2e96..1e3d2af4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserStatusComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUserStatusComposer.java @@ -1,5 +1,7 @@ package com.eu.habbo.messages.outgoing.rooms.users; +import com.eu.habbo.habbohotel.wired.core.WiredMoveCarryHelper; +import com.eu.habbo.habbohotel.wired.core.WiredUserMovementHelper; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; import com.eu.habbo.habbohotel.users.Habbo; @@ -8,7 +10,9 @@ import com.eu.habbo.messages.outgoing.MessageComposer; import com.eu.habbo.messages.outgoing.Outgoing; import gnu.trove.set.hash.THashSet; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Map; public class RoomUserStatusComposer extends MessageComposer { @@ -38,8 +42,20 @@ public class RoomUserStatusComposer extends MessageComposer { protected ServerMessage composeInternal() { this.response.init(Outgoing.RoomUserStatusComposer); if (this.roomUnits != null) { - this.response.appendInt(this.roomUnits.size()); + List roomUnits = new ArrayList<>(); + for (RoomUnit roomUnit : this.roomUnits) { + if (roomUnit == null + || WiredMoveCarryHelper.shouldSuppressStatusComposer(roomUnit) + || WiredUserMovementHelper.shouldSuppressStatusComposer(roomUnit)) { + continue; + } + + roomUnits.add(roomUnit); + } + + this.response.appendInt(roomUnits.size()); + for (RoomUnit roomUnit : roomUnits) { this.response.appendInt(roomUnit.getId()); this.response.appendInt(roomUnit.getPreviousLocation().x); this.response.appendInt(roomUnit.getPreviousLocation().y); @@ -59,8 +75,21 @@ public class RoomUserStatusComposer extends MessageComposer { } } else { synchronized (this.habbos) { - this.response.appendInt(this.habbos.size()); + List habbos = new ArrayList<>(); + for (Habbo habbo : this.habbos) { + if (habbo == null + || habbo.getRoomUnit() == null + || WiredMoveCarryHelper.shouldSuppressStatusComposer(habbo.getRoomUnit()) + || WiredUserMovementHelper.shouldSuppressStatusComposer(habbo.getRoomUnit())) { + continue; + } + + habbos.add(habbo); + } + + this.response.appendInt(habbos.size()); + for (Habbo habbo : habbos) { this.response.appendInt(habbo.getRoomUnit().getId()); this.response.appendInt(habbo.getRoomUnit().getPreviousLocation().x); this.response.appendInt(habbo.getRoomUnit().getPreviousLocation().y); 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 9efb6bf9..9d634f44 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 @@ -66,6 +66,8 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendString(""); this.response.appendInt(this.habbo.getHabboStats().getAchievementScore()); this.response.appendBoolean(true); + this.response.appendString(this.habbo.getHabboInfo().getRoomEntryMethod()); + this.response.appendInt(this.habbo.getHabboInfo().getRoomEntryTeleportId()); } else if (this.habbos != null) { this.response.appendInt(this.habbos.size()); for (Habbo habbo : this.habbos) { @@ -97,6 +99,8 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendString(""); this.response.appendInt(habbo.getHabboStats().getAchievementScore()); this.response.appendBoolean(true); + this.response.appendString(habbo.getHabboInfo().getRoomEntryMethod()); + this.response.appendInt(habbo.getHabboInfo().getRoomEntryTeleportId()); } } } else if (this.bot != null) { @@ -128,6 +132,8 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendShort(7); this.response.appendShort(8); this.response.appendShort(9); + this.response.appendString("unknown"); + this.response.appendInt(0); } else if (this.bots != null) { this.response.appendInt(this.bots.size()); for (Bot bot : this.bots) { @@ -158,6 +164,8 @@ public class RoomUsersComposer extends MessageComposer { this.response.appendShort(7); this.response.appendShort(8); this.response.appendShort(9); + this.response.appendString("unknown"); + this.response.appendInt(0); } } return this.response; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomBroadcastComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomBroadcastComposer.java new file mode 100644 index 00000000..7f8680b3 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomBroadcastComposer.java @@ -0,0 +1,31 @@ +package com.eu.habbo.messages.outgoing.rooms.youtube; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.List; + +public class YouTubeRoomBroadcastComposer extends MessageComposer { + private final String videoId; + private final String senderName; + private final List playlist; + + public YouTubeRoomBroadcastComposer(String videoId, String senderName, List playlist) { + this.videoId = videoId; + this.senderName = senderName; + this.playlist = playlist; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.YouTubeRoomBroadcastComposer); + this.response.appendString(this.videoId); + this.response.appendString(this.senderName); + this.response.appendInt(this.playlist.size()); + for (String id : this.playlist) { + this.response.appendString(id); + } + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomSettingsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomSettingsComposer.java new file mode 100644 index 00000000..64ff91cb --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomSettingsComposer.java @@ -0,0 +1,20 @@ +package com.eu.habbo.messages.outgoing.rooms.youtube; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class YouTubeRoomSettingsComposer extends MessageComposer { + private final boolean youtubeEnabled; + + public YouTubeRoomSettingsComposer(boolean youtubeEnabled) { + this.youtubeEnabled = youtubeEnabled; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.YouTubeRoomSettingsComposer); + this.response.appendInt(this.youtubeEnabled ? 1 : 0); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomWatchersComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomWatchersComposer.java new file mode 100644 index 00000000..d57a3463 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/youtube/YouTubeRoomWatchersComposer.java @@ -0,0 +1,25 @@ +package com.eu.habbo.messages.outgoing.rooms.youtube; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.Set; + +public class YouTubeRoomWatchersComposer extends MessageComposer { + private final Set watcherIds; + + public YouTubeRoomWatchersComposer(Set watcherIds) { + this.watcherIds = watcherIds; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.YouTubeRoomWatchersComposer); + this.response.appendInt(this.watcherIds.size()); + for (int id : this.watcherIds) { + this.response.appendInt(id); + } + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/AddUserBadgeComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/AddUserBadgeComposer.java index d8802b20..1e4216bf 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/AddUserBadgeComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/AddUserBadgeComposer.java @@ -7,9 +7,15 @@ import com.eu.habbo.messages.outgoing.Outgoing; public class AddUserBadgeComposer extends MessageComposer { private final HabboBadge badge; + private final String senderName; public AddUserBadgeComposer(HabboBadge badge) { + this(badge, ""); + } + + public AddUserBadgeComposer(HabboBadge badge, String senderName) { this.badge = badge; + this.senderName = senderName == null ? "" : senderName; } @Override @@ -17,10 +23,15 @@ public class AddUserBadgeComposer extends MessageComposer { this.response.init(Outgoing.AddUserBadgeComposer); this.response.appendInt(this.badge.getId()); this.response.appendString(this.badge.getCode()); + this.response.appendString(this.senderName); return this.response; } public HabboBadge getBadge() { return badge; } + + public String getSenderName() { + return senderName; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/InClientLinkComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/InClientLinkComposer.java new file mode 100644 index 00000000..61c8548f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/InClientLinkComposer.java @@ -0,0 +1,20 @@ +package com.eu.habbo.messages.outgoing.users; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class InClientLinkComposer extends MessageComposer { + private final String link; + + public InClientLinkComposer(String link) { + this.link = link; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.InClientLinkComposer); + this.response.appendString(this.link); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/MutedWhisperComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/MutedWhisperComposer.java index bae0cc7a..ef24051a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/MutedWhisperComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/MutedWhisperComposer.java @@ -8,7 +8,7 @@ public class MutedWhisperComposer extends MessageComposer { private final int seconds; public MutedWhisperComposer(int seconds) { - this.seconds = seconds; + this.seconds = Math.max(0, seconds); } @Override diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserClothesComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserClothesComposer.java index c0b5dfe3..5a902bfe 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserClothesComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/users/UserClothesComposer.java @@ -11,8 +11,19 @@ import gnu.trove.procedure.TIntProcedure; import java.util.ArrayList; public class UserClothesComposer extends MessageComposer { + private static class ClothEntry { + private final String name; + private final int[] setIds; + + private ClothEntry(String name, int[] setIds) { + this.name = name; + this.setIds = setIds; + } + } + private final ArrayList idList = new ArrayList<>(); private final ArrayList nameList = new ArrayList<>(); + private final ArrayList clothEntries = new ArrayList<>(); public UserClothesComposer(Habbo habbo) { habbo.getInventory().getWardrobeComponent().getClothing().forEach(new TIntProcedure() { @@ -31,6 +42,12 @@ public class UserClothesComposer extends MessageComposer { return true; } }); + + for (ClothItem item : Emulator.getGameEnvironment().getCatalogManager().clothing.values()) { + if (item != null) { + this.clothEntries.add(new ClothEntry(item.name, item.setId)); + } + } } @Override @@ -40,6 +57,17 @@ public class UserClothesComposer extends MessageComposer { this.idList.forEach(this.response::appendInt); this.response.appendInt(this.nameList.size()); this.nameList.forEach(this.response::appendString); + this.response.appendInt(this.clothEntries.size()); + + for (ClothEntry entry : this.clothEntries) { + this.response.appendString(entry.name); + this.response.appendInt(entry.setIds.length); + + for (int setId : entry.setIds) { + this.response.appendInt(setId); + } + } + return this.response; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredExtraDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredExtraDataComposer.java new file mode 100644 index 00000000..1028a6bb --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredExtraDataComposer.java @@ -0,0 +1,25 @@ +package com.eu.habbo.messages.outgoing.wired; + +import com.eu.habbo.habbohotel.items.interactions.InteractionWiredExtra; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class WiredExtraDataComposer extends MessageComposer { + private final InteractionWiredExtra extra; + private final Room room; + + public WiredExtraDataComposer(InteractionWiredExtra extra, Room room) { + this.extra = extra; + this.room = room; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.WiredEffectDataComposer); + this.extra.serializeWiredData(this.response, this.room); + this.extra.needsUpdate(true); + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredMonitorDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredMonitorDataComposer.java new file mode 100644 index 00000000..6f27665d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredMonitorDataComposer.java @@ -0,0 +1,84 @@ +package com.eu.habbo.messages.outgoing.wired; + +import com.eu.habbo.habbohotel.wired.core.WiredRoomDiagnostics; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class WiredMonitorDataComposer extends MessageComposer { + private final WiredRoomDiagnostics.Snapshot snapshot; + + public WiredMonitorDataComposer(WiredRoomDiagnostics.Snapshot snapshot) { + this.snapshot = snapshot; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.WiredMonitorDataComposer); + + if (this.snapshot == null) { + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendBoolean(false); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + this.response.appendInt(0); + return this.response; + } + + this.response.appendInt(this.snapshot.getUsageCurrentWindow()); + this.response.appendInt(this.snapshot.getUsageLimitPerWindow()); + this.response.appendBoolean(this.snapshot.isHeavy()); + this.response.appendInt(this.snapshot.getDelayedEventsPending()); + this.response.appendInt(this.snapshot.getDelayedEventsLimit()); + this.response.appendInt(this.snapshot.getAverageExecutionMs()); + this.response.appendInt(this.snapshot.getPeakExecutionMs()); + this.response.appendInt(this.snapshot.getRecursionDepthCurrent()); + this.response.appendInt(this.snapshot.getRecursionDepthLimit()); + this.response.appendInt(this.snapshot.getKilledRemainingSeconds()); + this.response.appendInt(this.snapshot.getUsageWindowMs()); + this.response.appendInt(this.snapshot.getOverloadAverageThresholdMs()); + this.response.appendInt(this.snapshot.getOverloadPeakThresholdMs()); + this.response.appendInt(this.snapshot.getHeavyUsageThresholdPercent()); + this.response.appendInt(this.snapshot.getHeavyConsecutiveWindowsThreshold()); + this.response.appendInt(this.snapshot.getOverloadConsecutiveWindowsThreshold()); + this.response.appendInt(this.snapshot.getHeavyDelayedThresholdPercent()); + this.response.appendInt(this.snapshot.getLogs().size()); + + for (WiredRoomDiagnostics.LogEntry log : this.snapshot.getLogs()) { + this.response.appendString(log.getType().name()); + this.response.appendString(log.getSeverity().name()); + this.response.appendInt(log.getCount()); + this.response.appendInt((int) (log.getLastOccurredAtMs() / 1000L)); + this.response.appendString(log.getLatestReason()); + this.response.appendString(log.getLatestSourceLabel()); + this.response.appendInt(log.getLatestSourceId()); + } + + this.response.appendInt(this.snapshot.getHistory().size()); + + for (WiredRoomDiagnostics.HistoryEntry historyEntry : this.snapshot.getHistory()) { + this.response.appendString(historyEntry.getType().name()); + this.response.appendString(historyEntry.getSeverity().name()); + this.response.appendInt((int) (historyEntry.getOccurredAtMs() / 1000L)); + this.response.appendString(historyEntry.getReason()); + this.response.appendString(historyEntry.getSourceLabel()); + this.response.appendInt(historyEntry.getSourceId()); + } + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredRoomSettingsDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredRoomSettingsDataComposer.java new file mode 100644 index 00000000..caf0dbd5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredRoomSettingsDataComposer.java @@ -0,0 +1,38 @@ +package com.eu.habbo.messages.outgoing.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class WiredRoomSettingsDataComposer extends MessageComposer { + private final Room room; + private final Habbo habbo; + + public WiredRoomSettingsDataComposer(Room room, Habbo habbo) { + this.room = room; + this.habbo = habbo; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.WiredRoomSettingsDataComposer); + + int roomId = (this.room != null) ? this.room.getId() : 0; + boolean canInspect = this.room != null && this.room.canInspectWired(this.habbo); + boolean canModify = this.room != null && this.room.canModifyWired(this.habbo); + boolean canManageSettings = this.room != null && this.room.canManageWiredSettings(this.habbo); + int inspectMask = canInspect ? this.room.getWiredInspectMask() : 0; + int modifyMask = canInspect ? this.room.getWiredModifyMask() : 0; + + this.response.appendInt(roomId); + this.response.appendInt(inspectMask); + this.response.appendInt(modifyMask); + this.response.appendBoolean(canInspect); + this.response.appendBoolean(canModify); + this.response.appendBoolean(canManageSettings); + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredUserVariablesDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredUserVariablesDataComposer.java new file mode 100644 index 00000000..d0f50d0b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredUserVariablesDataComposer.java @@ -0,0 +1,171 @@ +package com.eu.habbo.messages.outgoing.wired; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomFurniVariableManager; +import com.eu.habbo.habbohotel.rooms.RoomVariableManager; +import com.eu.habbo.habbohotel.rooms.RoomUserVariableManager; +import com.eu.habbo.habbohotel.rooms.WiredVariableDefinitionInfo; +import com.eu.habbo.habbohotel.wired.core.WiredContextVariableSupport; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.Collections; +import java.util.List; + +public class WiredUserVariablesDataComposer extends MessageComposer { + private final RoomUserVariableManager.Snapshot userSnapshot; + private final RoomFurniVariableManager.Snapshot furniSnapshot; + private final RoomVariableManager.Snapshot roomSnapshot; + private final List contextDefinitions; + + public WiredUserVariablesDataComposer(RoomUserVariableManager.Snapshot userSnapshot, RoomFurniVariableManager.Snapshot furniSnapshot, RoomVariableManager.Snapshot roomSnapshot) { + this(userSnapshot, furniSnapshot, roomSnapshot, resolveContextDefinitions(userSnapshot, furniSnapshot, roomSnapshot)); + } + + public WiredUserVariablesDataComposer(RoomUserVariableManager.Snapshot userSnapshot, RoomFurniVariableManager.Snapshot furniSnapshot, RoomVariableManager.Snapshot roomSnapshot, List contextDefinitions) { + this.userSnapshot = userSnapshot; + this.furniSnapshot = furniSnapshot; + this.roomSnapshot = roomSnapshot; + this.contextDefinitions = (contextDefinitions != null) ? contextDefinitions : Collections.emptyList(); + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.WiredUserVariablesDataComposer); + + int roomId = 0; + + if (this.userSnapshot != null) { + roomId = this.userSnapshot.getRoomId(); + } else if (this.furniSnapshot != null) { + roomId = this.furniSnapshot.getRoomId(); + } else if (this.roomSnapshot != null) { + roomId = this.roomSnapshot.getRoomId(); + } + + this.response.appendInt(roomId); + + this.response.appendInt((this.userSnapshot != null) ? this.userSnapshot.getDefinitions().size() : 0); + + if (this.userSnapshot != null) { + for (RoomUserVariableManager.DefinitionEntry definition : this.userSnapshot.getDefinitions()) { + this.response.appendInt(definition.getItemId()); + this.response.appendString(definition.getName()); + this.response.appendBoolean(definition.hasValue()); + this.response.appendInt(definition.getAvailability()); + this.response.appendBoolean(definition.isTextConnected()); + this.response.appendBoolean(definition.isReadOnly()); + } + } + + this.response.appendInt((this.userSnapshot != null) ? this.userSnapshot.getUsers().size() : 0); + + if (this.userSnapshot != null) { + for (RoomUserVariableManager.UserAssignmentsEntry user : this.userSnapshot.getUsers()) { + this.response.appendInt(user.getUserId()); + this.response.appendInt(user.getAssignments().size()); + + for (RoomUserVariableManager.AssignmentEntry assignment : user.getAssignments()) { + this.response.appendInt(assignment.getVariableItemId()); + this.response.appendBoolean(assignment.hasValue()); + this.response.appendInt((assignment.getValue() != null) ? assignment.getValue() : 0); + this.response.appendInt(assignment.getCreatedAt()); + this.response.appendInt(assignment.getUpdatedAt()); + } + } + } + + this.response.appendInt((this.furniSnapshot != null) ? this.furniSnapshot.getDefinitions().size() : 0); + + if (this.furniSnapshot != null) { + for (RoomFurniVariableManager.DefinitionEntry definition : this.furniSnapshot.getDefinitions()) { + this.response.appendInt(definition.getItemId()); + this.response.appendString(definition.getName()); + this.response.appendBoolean(definition.hasValue()); + this.response.appendInt(definition.getAvailability()); + this.response.appendBoolean(definition.isTextConnected()); + this.response.appendBoolean(definition.isReadOnly()); + } + } + + this.response.appendInt((this.furniSnapshot != null) ? this.furniSnapshot.getFurnis().size() : 0); + + if (this.furniSnapshot != null) { + for (RoomFurniVariableManager.FurniAssignmentsEntry furni : this.furniSnapshot.getFurnis()) { + this.response.appendInt(furni.getFurniId()); + this.response.appendInt(furni.getAssignments().size()); + + for (RoomFurniVariableManager.AssignmentEntry assignment : furni.getAssignments()) { + this.response.appendInt(assignment.getVariableItemId()); + this.response.appendBoolean(assignment.hasValue()); + this.response.appendInt((assignment.getValue() != null) ? assignment.getValue() : 0); + this.response.appendInt(assignment.getCreatedAt()); + this.response.appendInt(assignment.getUpdatedAt()); + } + } + } + + this.response.appendInt((this.roomSnapshot != null) ? this.roomSnapshot.getDefinitions().size() : 0); + + if (this.roomSnapshot != null) { + for (RoomVariableManager.DefinitionEntry definition : this.roomSnapshot.getDefinitions()) { + this.response.appendInt(definition.getItemId()); + this.response.appendString(definition.getName()); + this.response.appendBoolean(definition.hasValue()); + this.response.appendInt(definition.getAvailability()); + this.response.appendBoolean(definition.isTextConnected()); + this.response.appendBoolean(definition.isReadOnly()); + } + } + + this.response.appendInt((this.roomSnapshot != null) ? this.roomSnapshot.getAssignments().size() : 0); + + if (this.roomSnapshot != null) { + for (RoomVariableManager.AssignmentEntry assignment : this.roomSnapshot.getAssignments()) { + this.response.appendInt(assignment.getVariableItemId()); + this.response.appendBoolean(assignment.hasValue()); + this.response.appendInt((assignment.getValue() != null) ? assignment.getValue() : 0); + this.response.appendInt(assignment.getCreatedAt()); + this.response.appendInt(assignment.getUpdatedAt()); + } + } + + this.response.appendInt(this.contextDefinitions.size()); + + for (WiredVariableDefinitionInfo definition : this.contextDefinitions) { + if (definition == null) { + continue; + } + + this.response.appendInt(definition.getItemId()); + this.response.appendString(definition.getName()); + this.response.appendBoolean(definition.hasValue()); + this.response.appendInt(definition.getAvailability()); + this.response.appendBoolean(definition.isTextConnected()); + this.response.appendBoolean(definition.isReadOnly()); + } + + return this.response; + } + + private static List resolveContextDefinitions(RoomUserVariableManager.Snapshot userSnapshot, RoomFurniVariableManager.Snapshot furniSnapshot, RoomVariableManager.Snapshot roomSnapshot) { + int roomId = 0; + + if (userSnapshot != null) { + roomId = userSnapshot.getRoomId(); + } else if (furniSnapshot != null) { + roomId = furniSnapshot.getRoomId(); + } else if (roomSnapshot != null) { + roomId = roomSnapshot.getRoomId(); + } + + if (roomId <= 0) { + return Collections.emptyList(); + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId); + return room != null ? WiredContextVariableSupport.createDefinitionInfos(room) : Collections.emptyList(); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameMessageRateLimit.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameMessageRateLimit.java index 7a4c8de2..1b83a3ee 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameMessageRateLimit.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameMessageRateLimit.java @@ -6,13 +6,18 @@ import com.eu.habbo.messages.ClientMessage; import com.eu.habbo.networking.gameserver.GameServerAttributes; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageDecoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; public class GameMessageRateLimit extends MessageToMessageDecoder { + private static final Logger LOGGER = LoggerFactory.getLogger(GameMessageRateLimit.class); + private static final int RESET_TIME = 1; private static final int MAX_COUNTER = 10; + private static final int DEFAULT_GLOBAL_MAX = 50; @Override protected void decode(ChannelHandlerContext ctx, ClientMessage message, List out) throws Exception { @@ -23,26 +28,36 @@ public class GameMessageRateLimit extends MessageToMessageDecoder } int count = 0; + int globalCount = 0; - // Check if reset time has passed. int timestamp = Emulator.getIntUnixTimestamp(); if (timestamp - client.lastPacketCounterCleared > RESET_TIME) { - // Reset counter. client.incomingPacketCounter.clear(); client.lastPacketCounterCleared = timestamp; } else { - // Get stored count for message id. count = client.incomingPacketCounter.getOrDefault(message.getMessageId(), 0); + for (int c : client.incomingPacketCounter.values()) { + globalCount += c; + } } - // If we exceeded the counter, drop the packet. if (count > MAX_COUNTER) { return; } + int globalMax = Emulator.getConfig().getInt("packet.global.rate.limit", DEFAULT_GLOBAL_MAX); + if (globalCount > globalMax) { + if (globalCount == globalMax + 1) { + String username = (client.getHabbo() != null && client.getHabbo().getHabboInfo() != null) + ? client.getHabbo().getHabboInfo().getUsername() : "unauthenticated"; + LOGGER.warn("Global packet rate limit exceeded for {} ({} packets/sec) — dropping excess packets", + username, globalCount); + } + return; + } + client.incomingPacketCounter.put(message.getMessageId(), ++count); - // Continue processing. out.add(message); } diff --git a/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java b/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java index d4bbc1c7..5712a3b8 100644 --- a/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java +++ b/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java @@ -120,6 +120,20 @@ public class PluginManager { WiredEngine.MAX_EVENTS_PER_WINDOW = Emulator.getConfig().getInt("wired.abuse.max.events.per.window", 100); WiredEngine.RATE_LIMIT_WINDOW_MS = Emulator.getConfig().getInt("wired.abuse.rate.limit.window.ms", 10000); WiredEngine.WIRED_BAN_DURATION_MS = Emulator.getConfig().getInt("wired.abuse.ban.duration.ms", 600000); + WiredEngine.MONITOR_USAGE_WINDOW_MS = Emulator.getConfig().getInt("wired.monitor.usage.window.ms", 1000); + WiredEngine.MONITOR_USAGE_LIMIT = Emulator.getConfig().getInt("wired.monitor.usage.limit", 1000); + WiredEngine.MONITOR_DELAYED_EVENTS_LIMIT = Emulator.getConfig().getInt("wired.monitor.delayed.events.limit", 100); + WiredEngine.MONITOR_OVERLOAD_AVERAGE_MS = Emulator.getConfig().getInt("wired.monitor.overload.average.ms", 50); + WiredEngine.MONITOR_OVERLOAD_PEAK_MS = Emulator.getConfig().getInt("wired.monitor.overload.peak.ms", 150); + WiredEngine.MONITOR_OVERLOAD_CONSECUTIVE_WINDOWS = Emulator.getConfig().getInt("wired.monitor.overload.consecutive.windows", 2); + WiredEngine.MONITOR_HEAVY_USAGE_PERCENT = Emulator.getConfig().getInt("wired.monitor.heavy.usage.percent", 70); + WiredEngine.MONITOR_HEAVY_CONSECUTIVE_WINDOWS = Emulator.getConfig().getInt("wired.monitor.heavy.consecutive.windows", 5); + WiredEngine.MONITOR_HEAVY_DELAYED_PERCENT = Emulator.getConfig().getInt("wired.monitor.heavy.delayed.percent", 60); + + if (WiredManager.getEngine() != null) { + WiredManager.getEngine().clearAllDiagnostics(); + } + NavigatorManager.MAXIMUM_RESULTS_PER_PAGE = Emulator.getConfig().getInt("hotel.navigator.search.maxresults"); NavigatorManager.CATEGORY_SORT_USING_ORDER_NUM = Emulator.getConfig().getBoolean("hotel.navigator.sort.ordernum"); RoomChatMessage.MAXIMUM_LENGTH = Emulator.getConfig().getInt("hotel.chat.max.length"); diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/HabboGiveHandItemToHabbo.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/HabboGiveHandItemToHabbo.java index 16a59ec5..a3cf6470 100644 --- a/Emulator/src/main/java/com/eu/habbo/threading/runnables/HabboGiveHandItemToHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/threading/runnables/HabboGiveHandItemToHabbo.java @@ -1,6 +1,8 @@ package com.eu.habbo.threading.runnables; import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomHanditemBlockSupport; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserHandItemComposer; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserReceivedHandItemComposer; @@ -21,6 +23,12 @@ public class HabboGiveHandItemToHabbo implements Runnable { if (this.from.getHabboInfo().getCurrentRoom() != this.target.getHabboInfo().getCurrentRoom()) return; + Room room = this.from.getHabboInfo().getCurrentRoom(); + + if (RoomHanditemBlockSupport.isHanditemBlocked(room)) { + return; + } + int itemId = this.from.getRoomUnit().getHandItem(); if (itemId > 0) { diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/RoomUnitTeleport.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/RoomUnitTeleport.java index 9747026e..43ddeb97 100644 --- a/Emulator/src/main/java/com/eu/habbo/threading/runnables/RoomUnitTeleport.java +++ b/Emulator/src/main/java/com/eu/habbo/threading/runnables/RoomUnitTeleport.java @@ -5,6 +5,7 @@ import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,6 +39,8 @@ public class RoomUnitTeleport implements Runnable { return; } + WiredFreezeUtil.onTeleport(this.room, this.roomUnit); + RoomTile lastLocation = this.roomUnit.getCurrentLocation(); RoomTile newLocation = this.room.getLayout().getTile((short) this.x, (short) this.y); @@ -60,6 +63,7 @@ public class RoomUnitTeleport implements Runnable { //this.room.sendComposer(teleportMessage); this.roomUnit.statusUpdate(true); roomUnit.isWiredTeleporting = false; + WiredFreezeUtil.restoreWalkState(this.roomUnit); this.room.updateHabbosAt(newLocation.x, newLocation.y); this.room.updateBotsAt(newLocation.x, newLocation.y); diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/games/GameUpCounter.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/games/GameUpCounter.java new file mode 100644 index 00000000..19e45f5c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/threading/runnables/games/GameUpCounter.java @@ -0,0 +1,46 @@ +package com.eu.habbo.threading.runnables.games; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.interactions.games.InteractionGameUpCounter; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.wired.core.WiredManager; + +public class GameUpCounter implements Runnable { + private final InteractionGameUpCounter timer; + + public GameUpCounter(InteractionGameUpCounter timer) { + this.timer = timer; + } + + @Override + public void run() { + if (timer.getRoomId() == 0) { + timer.setRunning(false); + timer.setThreadActive(false); + return; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(timer.getRoomId()); + + if (room == null || !timer.isRunning() || timer.isPaused()) { + timer.setThreadActive(false); + return; + } + + int tickDelayMs = (int) timer.getNextTickDelayMs(); + timer.advanceCounterInMs(tickDelayMs); + WiredManager.triggerClockCounter(room, timer); + + if (timer.getCurrentTimeInMs() < timer.getMaximumTimeInMs()) { + timer.setThreadActive(true); + Emulator.getThreading().run(this, timer.getNextTickDelayMs()); + } else { + timer.setThreadActive(false); + timer.setCurrentTimeInMs(timer.getMaximumTimeInMs()); + timer.endGame(room); + WiredManager.triggerGameEnds(room); + } + + room.updateItem(timer); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionFive.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionFive.java index 9fab330b..ea5d1ee8 100644 --- a/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionFive.java +++ b/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionFive.java @@ -5,6 +5,7 @@ import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.interactions.InteractionTeleportTile; import com.eu.habbo.habbohotel.rooms.*; import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; import com.eu.habbo.threading.runnables.HabboItemNewState; import com.eu.habbo.threading.runnables.RoomUnitWalkToLocation; @@ -46,6 +47,7 @@ class TeleportActionFive implements Runnable { List onSuccess = new ArrayList(); onSuccess.add(() -> { unit.setCanLeaveRoomByDoor(true); + WiredFreezeUtil.restoreWalkState(unit); Emulator.getThreading().run(() -> { unit.isLeavingTeleporter = false; @@ -57,6 +59,8 @@ class TeleportActionFive implements Runnable { unit.statusUpdate(true); unit.isLeavingTeleporter = true; Emulator.getThreading().run(new RoomUnitWalkToLocation(unit, tile, room, onSuccess, onSuccess)); + } else { + WiredFreezeUtil.restoreWalkState(unit); } this.currentTeleport.setExtradata("1"); diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionFour.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionFour.java index 5fea22ef..7b13dec4 100644 --- a/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionFour.java +++ b/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionFour.java @@ -4,6 +4,7 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; class TeleportActionFour implements Runnable { private final HabboItem currentTeleport; @@ -21,7 +22,7 @@ class TeleportActionFour implements Runnable { if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != this.room) { this.client.getHabbo().getHabboInfo().setLoadingRoom(0); this.client.getHabbo().getRoomUnit().isTeleporting = false; - this.client.getHabbo().getRoomUnit().setCanWalk(true); + WiredFreezeUtil.restoreWalkState(this.client.getHabbo().getRoomUnit()); this.currentTeleport.setExtradata("0"); this.room.updateItem(this.currentTeleport); return; diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionOne.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionOne.java index 757db39c..8ba24c37 100644 --- a/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionOne.java +++ b/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionOne.java @@ -7,6 +7,7 @@ import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; import com.eu.habbo.habbohotel.rooms.RoomUserRotation; import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer; public class TeleportActionOne implements Runnable { @@ -25,7 +26,7 @@ public class TeleportActionOne implements Runnable { if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != this.room) { this.client.getHabbo().getHabboInfo().setLoadingRoom(0); this.client.getHabbo().getRoomUnit().isTeleporting = false; - this.client.getHabbo().getRoomUnit().setCanWalk(true); + WiredFreezeUtil.restoreWalkState(this.client.getHabbo().getRoomUnit()); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionThree.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionThree.java index ed3a5d84..efcd9ca7 100644 --- a/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionThree.java +++ b/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionThree.java @@ -9,6 +9,7 @@ import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; import com.eu.habbo.habbohotel.rooms.RoomUserRotation; import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; class TeleportActionThree implements Runnable { private final HabboItem currentTeleport; @@ -26,7 +27,7 @@ class TeleportActionThree implements Runnable { if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != this.room) { this.client.getHabbo().getHabboInfo().setLoadingRoom(0); this.client.getHabbo().getRoomUnit().isTeleporting = false; - this.client.getHabbo().getRoomUnit().setCanWalk(true); + WiredFreezeUtil.restoreWalkState(this.client.getHabbo().getRoomUnit()); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionTwo.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionTwo.java index eb5c4bb0..ceab30ff 100644 --- a/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionTwo.java +++ b/Emulator/src/main/java/com/eu/habbo/threading/runnables/teleport/TeleportActionTwo.java @@ -7,19 +7,11 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionTeleportTile; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnitStatus; import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.core.WiredFreezeUtil; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer; import com.eu.habbo.threading.runnables.HabboItemNewState; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; class TeleportActionTwo implements Runnable { - private static final Logger LOGGER = LoggerFactory.getLogger(TeleportActionTwo.class); - private final HabboItem currentTeleport; private final Room room; private final GameClient client; @@ -41,7 +33,7 @@ class TeleportActionTwo implements Runnable { if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != this.room) { this.client.getHabbo().getHabboInfo().setLoadingRoom(0); this.client.getHabbo().getRoomUnit().isTeleporting = false; - this.client.getHabbo().getRoomUnit().setCanWalk(true); + WiredFreezeUtil.restoreWalkState(this.client.getHabbo().getRoomUnit()); return; } @@ -64,23 +56,11 @@ class TeleportActionTwo implements Runnable { ((InteractionTeleport) this.currentTeleport).setTargetId(0); } if (((InteractionTeleport) this.currentTeleport).getTargetId() == 0) { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT items_teleports.*, A.room_id as a_room_id, A.id as a_id, B.room_id as b_room_id, B.id as b_id FROM items_teleports INNER JOIN items AS A ON items_teleports.teleport_one_id = A.id INNER JOIN items AS B ON items_teleports.teleport_two_id = B.id WHERE (teleport_one_id = ? OR teleport_two_id = ?)")) { - statement.setInt(1, this.currentTeleport.getId()); - statement.setInt(2, this.currentTeleport.getId()); + int[] targetTeleport = Emulator.getGameEnvironment().getItemManager().getTargetTeleportRoomId(this.currentTeleport); - try (ResultSet set = statement.executeQuery()) { - if (set.next()) { - if (set.getInt("a_id") != this.currentTeleport.getId()) { - ((InteractionTeleport) this.currentTeleport).setTargetId(set.getInt("a_id")); - ((InteractionTeleport) this.currentTeleport).setTargetRoomId(set.getInt("a_room_id")); - } else { - ((InteractionTeleport) this.currentTeleport).setTargetId(set.getInt("b_id")); - ((InteractionTeleport) this.currentTeleport).setTargetRoomId(set.getInt("b_room_id")); - } - } - } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); + if (targetTeleport.length == 2) { + ((InteractionTeleport) this.currentTeleport).setTargetRoomId(targetTeleport[0]); + ((InteractionTeleport) this.currentTeleport).setTargetId(targetTeleport[1]); } } diff --git a/Emulator/src/main/java/com/eu/habbo/util/HotelDateTimeUtil.java b/Emulator/src/main/java/com/eu/habbo/util/HotelDateTimeUtil.java new file mode 100644 index 00000000..0abb37f3 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/util/HotelDateTimeUtil.java @@ -0,0 +1,59 @@ +package com.eu.habbo.util; + +import com.eu.habbo.Emulator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +public final class HotelDateTimeUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(HotelDateTimeUtil.class); + private static final String CONFIG_KEY = "hotel.timezone"; + private static volatile String lastInvalidTimezoneId = null; + + private HotelDateTimeUtil() { + } + + public static String getTimezoneId() { + return getZoneId().getId(); + } + + public static ZoneId getZoneId() { + String configuredZoneId = Emulator.getConfig().getValue(CONFIG_KEY, ZoneId.systemDefault().getId()); + + try { + lastInvalidTimezoneId = null; + return ZoneId.of(configuredZoneId.trim()); + } catch (Exception e) { + if (!configuredZoneId.equals(lastInvalidTimezoneId)) { + LOGGER.warn("Invalid {} '{}', falling back to system timezone '{}'.", CONFIG_KEY, configuredZoneId, ZoneId.systemDefault().getId()); + lastInvalidTimezoneId = configuredZoneId; + } + return ZoneId.systemDefault(); + } + } + + public static ZonedDateTime now() { + return ZonedDateTime.now(getZoneId()); + } + + public static LocalDateTime localDateTimeNow() { + return LocalDateTime.now(getZoneId()); + } + + public static LocalDate localDateNow() { + return LocalDate.now(getZoneId()); + } + + public static LocalTime localTimeNow() { + return LocalTime.now(getZoneId()); + } + + public static long toEpochSecond(LocalDateTime dateTime) { + return dateTime.atZone(getZoneId()).toEpochSecond(); + } +} diff --git a/Emulator/src/main/resources/logback.xml b/Emulator/src/main/resources/logback.xml index 7c9f1ce7..f4a62d3f 100644 --- a/Emulator/src/main/resources/logback.xml +++ b/Emulator/src/main/resources/logback.xml @@ -56,6 +56,8 @@ + + diff --git a/Latest_Compiled_Version/Habbo-4.0.5-jar-with-dependencies.jar b/Latest_Compiled_Version/Habbo-4.1.1-jar-with-dependencies.jar similarity index 79% rename from Latest_Compiled_Version/Habbo-4.0.5-jar-with-dependencies.jar rename to Latest_Compiled_Version/Habbo-4.1.1-jar-with-dependencies.jar index 7c845e2b..bfb8e246 100644 Binary files a/Latest_Compiled_Version/Habbo-4.0.5-jar-with-dependencies.jar and b/Latest_Compiled_Version/Habbo-4.1.1-jar-with-dependencies.jar differ diff --git a/Latest_Compiled_Version/config.ini.example b/Latest_Compiled_Version/config.ini.example index 1d05c91c..e1eee315 100644 --- a/Latest_Compiled_Version/config.ini.example +++ b/Latest_Compiled_Version/config.ini.example @@ -29,6 +29,14 @@ ws.whitelist=localhost #Header name for real client IP when behind a proxy (e.g., X-Forwarded-For, CF-Connecting-IP). Leave empty if not using a proxy. ws.ip.header= +# Databse configuration +db.pool.connection_timeout_ms = 10000 +db.pool.idle_timeout_ms = 600000 +db.pool.max_lifetime_ms = 1800000 +db.pool.validation_timeout_ms = 5000 +# set db.pool.leak_detection_ms to 0 to disable +db.pool.leak_detection_ms = 20000 set to 0 to disable + enc.enabled=false enc.e=3 enc.n=86851dd364d5c5cece3c883171cc6ddc5760779b992482bd1e20dd296888df91b33b936a7b93f06d29e8870f703a216257dec7c81de0058fea4cc5116f75e6efc4e9113513e45357dc3fd43d4efab5963ef178b78bd61e81a14c603b24c8bcce0a12230b320045498edc29282ff0603bc7b7dae8fc1b05b52b2f301a9dc783b7 diff --git a/Plugins/Camera/.gitattributes b/Plugins/Camera/.gitattributes deleted file mode 100644 index dfe07704..00000000 --- a/Plugins/Camera/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/Plugins/Camera/.gitignore b/Plugins/Camera/.gitignore deleted file mode 100644 index 27b37624..00000000 --- a/Plugins/Camera/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar -.mvn/wrapper/maven-wrapper.jar - -# Eclipse m2e generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) -.classpath - -.idea diff --git a/Plugins/Camera/Compiled/Camera-1.3.jar b/Plugins/Camera/Compiled/Camera-1.3.jar deleted file mode 100644 index 65c5e7f2..00000000 Binary files a/Plugins/Camera/Compiled/Camera-1.3.jar and /dev/null differ diff --git a/Plugins/Camera/Compiled/Camera-1.4.jar b/Plugins/Camera/Compiled/Camera-1.4.jar deleted file mode 100644 index af4edf16..00000000 Binary files a/Plugins/Camera/Compiled/Camera-1.4.jar and /dev/null differ diff --git a/Plugins/Camera/Compiled/Camera-1.6.jar b/Plugins/Camera/Compiled/Camera-1.6.jar deleted file mode 100644 index 2c3c32a2..00000000 Binary files a/Plugins/Camera/Compiled/Camera-1.6.jar and /dev/null differ diff --git a/Plugins/Camera/LICENSE b/Plugins/Camera/LICENSE deleted file mode 100644 index 113d1a33..00000000 --- a/Plugins/Camera/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 ThisAccountHasBeenSuspended, EntenKoeniq, habbo.sx - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Plugins/Camera/README.md b/Plugins/Camera/README.md deleted file mode 100644 index 75afa562..00000000 --- a/Plugins/Camera/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# 📷 Camera - -**New features compared to Apollyon** -- [x] Small and more efficient than Apollyon. -- [x] `*_small.png` are really small (original / 2) and not just copied. -- [x] Creates a folder structure if it doesn't exist. -- [x] Report failures to Habbo. -- [x] Added a cooldown time for taking photos to prevent your hard drive from filling up. -- [x] Prevent exploits on thumbnails, not just photos. -- [x] No more FTP (no one used that). - -[Download the latest compiled version here!](https://github.com/duckietm/Arcturus-Morningstar-Extended/blob/main/Plugins/Camera/Compiled/Camera-1.6.jar) - -## Informations -All missing settings in the database will be created with this plugin after the first launch of your emulator. -### emulator_settings -- `camera.url` -- - The URL from which you can access the image (default: http://localhost/camera/) -- `imager.location.output.camera` -- - The location where to save the photo (default: C:/inetpub/wwwroot/public/camera/) -- `imager.location.output.thumbnail` -- - The location to save the thumbnail (default: C:/inetpub/wwwroot/public/camera/thumbnails/) -- `camera.price.points.publish` -- - The price of points for publishing this photo (default: 1) -- `camera.price.points.publish.type` -- - The type of points to publish (default: 5 = diamonds) -- `camera.publish.delay` -- - The time, in seconds, how long the user should wait to post a new photo (default: 180) -- `camera.price.credits` -- - The credits required to purchase this photo (default: 2) -- `camera.price.points` -- - The price of points to purchase this photo (default: 0 = no points required) -- `camera.price.points.type` -- - The type of points to buy this photo (default: 5 = diamonds) -- `camera.render.delay` -- - The time, in seconds, how long the user should wait to take a new photo (default: 5) -### emulator_texts -- `camera.permission` -- - default: "You don't have permission to use the camera!" -- `camera.wait` -- - default: "Please wait %seconds% seconds before making another picture." -- `camera.error.creation` -- - default: "Failed to create your picture. \*sadpanda\*" - -Since this plugin doesn't use FTP like “Apollyon”, you can delete these values ​​in your database under `emulator_settings` if necessary. - -## Comparison -Apollyon doesn't show you any errors like you don't have enough currency. - -# Credits - -- EntenKoeniq for releaseing the fixes and rewritten the plugin. (This camera plugin was written for https://habbo.sx/ and is now available to the community for free.) -- John -- Beny (Packet Hijacking) -- Ovflowd (Original Implimentation) -- Alejandro (Error Handling Help) diff --git a/Plugins/Camera/pom.xml b/Plugins/Camera/pom.xml deleted file mode 100644 index 0690ec1b..00000000 --- a/Plugins/Camera/pom.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - 4.0.0 - - com.eu.camera - Camera - 1.6 - - - - org.apache.maven.plugins - maven-compiler-plugin - - 11 - 11 - - - - - - - UTF-8 - UTF-8 - 1.8 - 1.8 - - - - - com.eu.habbo - Habbo - 3.5.2 - - - \ No newline at end of file diff --git a/Plugins/Camera/src/main/java/com/eu/camera/Main.java b/Plugins/Camera/src/main/java/com/eu/camera/Main.java deleted file mode 100644 index 778abd8e..00000000 --- a/Plugins/Camera/src/main/java/com/eu/camera/Main.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.eu.camera; - -import com.eu.camera.handlers.PublishPhotoEvent; -import com.eu.camera.handlers.PurchasePhotoEvent; -import com.eu.camera.handlers.RenderRoomEvent; -import com.eu.camera.handlers.RenderRoomThumbnailEvent; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.users.Habbo; -import com.eu.habbo.messages.PacketManager; -import com.eu.habbo.messages.incoming.Incoming; -import com.eu.habbo.messages.incoming.MessageHandler; -import com.eu.habbo.plugin.EventHandler; -import com.eu.habbo.plugin.EventListener; -import com.eu.habbo.plugin.HabboPlugin; -import com.eu.habbo.plugin.events.emulator.EmulatorLoadedEvent; - -import gnu.trove.map.hash.THashMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.lang.reflect.Field; - -public class Main extends HabboPlugin implements EventListener { - private static final Logger LOGGER = LoggerFactory.getLogger(Emulator.class); - - @Override - public void onEnable() throws Exception { - Emulator.getPluginManager().registerEvents(this, this); - - if (Emulator.isReady && !Emulator.isShuttingDown) { - this.onEmulatorLoadedEvent(null); - } - } - - @Override - public void onDisable() throws Exception { - PacketManager packetManager = Emulator.getGameServer().getPacketManager(); - Field incomingField = PacketManager.class.getDeclaredField("incoming"); - incomingField.setAccessible(true); - @SuppressWarnings("unchecked") - THashMap> incoming = (THashMap>)incomingField.get(packetManager); - - // Removes the custom handlers for these packets. - incoming.remove(Incoming.CameraPublishToWebEvent, PublishPhotoEvent.class); - incoming.remove(Incoming.CameraPurchaseEvent, PurchasePhotoEvent.class); - incoming.remove(Incoming.CameraRoomPictureEvent, RenderRoomEvent.class); - incoming.remove(Incoming.CameraRoomThumbnailEvent, RenderRoomThumbnailEvent.class); - } - - @Override - public boolean hasPermission(Habbo habbo, String string) { - return false; - } - - @EventHandler - public void onEmulatorLoadedEvent(EmulatorLoadedEvent event) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException, Exception { - // Adds missing SQLs if they are not found. - Emulator.getConfig().register("camera.url", "http://localhost/camera/"); - Emulator.getConfig().register("imager.location.output.camera", "C:/inetpub/wwwroot/public/camera/"); - Emulator.getConfig().register("imager.location.output.thumbnail", "C:/inetpub/wwwroot/camera/thumbnails/"); - Emulator.getConfig().register("camera.price.points.publish", "1"); - Emulator.getConfig().register("camera.price.points.publish.type", "5"); - Emulator.getConfig().register("camera.publish.delay", "180"); - Emulator.getConfig().register("camera.price.credits", "2"); - Emulator.getConfig().register("camera.price.points", "0"); - Emulator.getConfig().register("camera.price.points.type", "5"); - Emulator.getConfig().register("camera.render.delay", "5"); - Emulator.getTexts().register("camera.permission", "You don't have permission to use the camera!"); - Emulator.getTexts().register("camera.wait", "Please wait %seconds% seconds before making another picture."); - Emulator.getTexts().register("camera.error.creation", "Failed to create your picture. *sadpanda*"); - - PacketManager packetManager = Emulator.getGameServer().getPacketManager(); - Field incomingField = PacketManager.class.getDeclaredField("incoming"); - incomingField.setAccessible(true); - @SuppressWarnings("unchecked") - THashMap> incoming = (THashMap>)incomingField.get(packetManager); - - // Removes the current handlers for these packets. - incoming.remove(Incoming.CameraPublishToWebEvent); - incoming.remove(Incoming.CameraPurchaseEvent); - incoming.remove(Incoming.CameraRoomPictureEvent); - incoming.remove(Incoming.CameraRoomThumbnailEvent); - - // Adds the new handlers for these packets. - packetManager.registerHandler(Incoming.CameraPublishToWebEvent, PublishPhotoEvent.class); - packetManager.registerHandler(Incoming.CameraPurchaseEvent, PurchasePhotoEvent.class); - packetManager.registerHandler(Incoming.CameraRoomPictureEvent, RenderRoomEvent.class); - packetManager.registerHandler(Incoming.CameraRoomThumbnailEvent, RenderRoomThumbnailEvent.class); - - // Create the output directories if they don't exist. - File outputDir = new File(Emulator.getConfig().getValue("imager.location.output.thumbnail")); - if (!outputDir.exists()) { - outputDir.mkdirs(); - } - - LOGGER.info("[Camera] Plugin has loaded!"); - } -} diff --git a/Plugins/Camera/src/main/java/com/eu/camera/handlers/PublishPhotoEvent.java b/Plugins/Camera/src/main/java/com/eu/camera/handlers/PublishPhotoEvent.java deleted file mode 100644 index e54565a1..00000000 --- a/Plugins/Camera/src/main/java/com/eu/camera/handlers/PublishPhotoEvent.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.eu.camera.handlers; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.users.Habbo; -import com.eu.habbo.habbohotel.users.HabboInfo; -import com.eu.habbo.messages.incoming.MessageHandler; -import com.eu.habbo.messages.outgoing.camera.CameraPublishWaitMessageComposer; -import com.eu.habbo.messages.outgoing.catalog.NotEnoughPointsTypeComposer; -import com.eu.habbo.plugin.events.users.UserPublishPictureEvent; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; - -public class PublishPhotoEvent extends MessageHandler { - public static final int CAMERA_PUBLISH_POINTS = Emulator.getConfig().getInt("camera.price.points.publish", 1); - public static final int CAMERA_PUBLISH_POINTS_TYPE = Emulator.getConfig().getInt("camera.price.points.publish.type", 5); - public static final int CAMERA_PUBLISH_DELAY = Emulator.getConfig().getInt("camera.publish.delay", 180); - - @Override - public void handle() { - Habbo habbo = this.client.getHabbo(); - HabboInfo habboInfo = habbo.getHabboInfo(); - - int points = habboInfo.getCurrencyAmount(CAMERA_PUBLISH_POINTS_TYPE); - - if (points < CAMERA_PUBLISH_POINTS) { - String currencyName = Emulator.getTexts().getValue("seasonal.name." + Integer.toString(CAMERA_PUBLISH_POINTS_TYPE), "currency"); - String alertMessage = "You don't have enough " + currencyName + "!"; - habbo.alert(alertMessage); - this.client.sendResponse(new NotEnoughPointsTypeComposer(false, true, CAMERA_PUBLISH_POINTS_TYPE)); - return; - } - - int photoTimestamp = habboInfo.getPhotoTimestamp(); - String photoJSON = habboInfo.getPhotoJSON(); - - if (photoTimestamp == 0 || photoJSON.isEmpty() || !photoJSON.contains(Integer.toString(photoTimestamp))) { - return; - } - - int currentTimestamp = Emulator.getIntUnixTimestamp(); - int timeSinceLastPublish = currentTimestamp - habboInfo.getWebPublishTimestamp(); - - if (timeSinceLastPublish < CAMERA_PUBLISH_DELAY) { - int wait = CAMERA_PUBLISH_DELAY - timeSinceLastPublish; - this.client.sendResponse(new CameraPublishWaitMessageComposer(false, wait, habboInfo.getPhotoURL())); - } else { - UserPublishPictureEvent publishPictureEvent = new UserPublishPictureEvent(habbo, habboInfo.getPhotoURL(), currentTimestamp, habboInfo.getPhotoRoomId()); - - if (!Emulator.getPluginManager().fireEvent(publishPictureEvent).isCancelled()) { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); - PreparedStatement statement = connection.prepareStatement("INSERT INTO camera_web (user_id, room_id, timestamp, url) VALUES (?, ?, ?, ?)")) { - statement.setInt(1, habboInfo.getId()); - statement.setInt(2, publishPictureEvent.roomId); - statement.setInt(3, publishPictureEvent.timestamp); - statement.setString(4, publishPictureEvent.URL); - statement.execute(); - - habboInfo.setWebPublishTimestamp(currentTimestamp); - - habbo.givePoints(CAMERA_PUBLISH_POINTS_TYPE, -CAMERA_PUBLISH_POINTS); - } catch (SQLException throwable) { - throwable.printStackTrace(); - } - } - - this.client.sendResponse(new CameraPublishWaitMessageComposer(true, 0, "")); - } - } -} diff --git a/Plugins/Camera/src/main/java/com/eu/camera/handlers/PurchasePhotoEvent.java b/Plugins/Camera/src/main/java/com/eu/camera/handlers/PurchasePhotoEvent.java deleted file mode 100644 index 4bc2b6cb..00000000 --- a/Plugins/Camera/src/main/java/com/eu/camera/handlers/PurchasePhotoEvent.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.eu.camera.handlers; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.achievements.AchievementManager; -import com.eu.habbo.habbohotel.items.Item; -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.messages.incoming.MessageHandler; -import com.eu.habbo.messages.outgoing.camera.CameraPurchaseSuccesfullComposer; -import com.eu.habbo.messages.outgoing.catalog.NotEnoughPointsTypeComposer; -import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer; -import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer; -import com.eu.habbo.plugin.events.users.UserPurchasePictureEvent; - -public class PurchasePhotoEvent extends MessageHandler { - public static final int CAMERA_PURCHASE_CREDITS = Emulator.getConfig().getInt("camera.price.credits", 2); - public static final int CAMERA_PURCHASE_POINTS = Emulator.getConfig().getInt("camera.price.points", 0); - public static final int CAMERA_PURCHASE_POINTS_TYPE = Emulator.getConfig().getInt("camera.price.points.type", 5); - public static final int CAMERA_ITEM_ID = Emulator.getConfig().getInt("camera.item_id"); - public static final String EXTERNAL_IMAGE_INTERACTION = "external_image"; - - @Override - public void handle() { - Habbo habbo = this.client.getHabbo(); - HabboInfo habboInfo = habbo.getHabboInfo(); - - if (habboInfo.getCredits() < CAMERA_PURCHASE_CREDITS) { - handleInsufficientCredits(habbo); - return; - } - - if (habboInfo.getCurrencyAmount(CAMERA_PURCHASE_POINTS_TYPE) < CAMERA_PURCHASE_POINTS) { - handleInsufficientPoints(habbo); - return; - } - - if (!isValidPhoto(habboInfo)) { - return; - } - - if (Emulator.getPluginManager().fireEvent(new UserPurchasePictureEvent(habbo, habboInfo.getPhotoURL(), habboInfo.getCurrentRoom().getId(), habboInfo.getPhotoTimestamp())).isCancelled()) { - return; - } - - Item item = Emulator.getGameEnvironment().getItemManager().getItem(CAMERA_ITEM_ID); - if (item == null || !item.getInteractionType().getName().equals(EXTERNAL_IMAGE_INTERACTION)) { - return; - } - - handlePurchasedPhoto(habbo, habboInfo, item); - } - - private void handleInsufficientCredits(Habbo habbo) { - habbo.alert("You don't have enough credits!"); - this.client.sendResponse(new NotEnoughPointsTypeComposer(true, false, 0)); - } - - private void handleInsufficientPoints(Habbo habbo) { - String alertMessage = "You don't have enough " + Emulator.getTexts().getValue("seasonal.name." + Integer.toString(CAMERA_PURCHASE_POINTS_TYPE), "currency") + "!"; - habbo.alert(alertMessage); - this.client.sendResponse(new NotEnoughPointsTypeComposer(false, true, CAMERA_PURCHASE_POINTS_TYPE)); - } - - private boolean isValidPhoto(HabboInfo habboInfo) { - return habboInfo.getPhotoTimestamp() != 0 && !habboInfo.getPhotoJSON().isEmpty() && habboInfo.getPhotoJSON().contains(Integer.toString(habboInfo.getPhotoTimestamp())); - } - - private void handlePurchasedPhoto(Habbo habbo, HabboInfo habboInfo, Item item) { - HabboItem photoItem = Emulator.getGameEnvironment().getItemManager().createItem(habboInfo.getId(), item, 0, 0, habboInfo.getPhotoJSON()); - - if (photoItem != null) { - photoItem.setExtradata(photoItem.getExtradata().replace("%id%", Integer.toString(photoItem.getId()))); - photoItem.needsUpdate(true); - - habbo.getInventory().getItemsComponent().addItem(photoItem); - - this.client.sendResponse(new CameraPurchaseSuccesfullComposer()); - this.client.sendResponse(new AddHabboItemComposer(photoItem)); - this.client.sendResponse(new InventoryRefreshComposer()); - - habbo.giveCredits(-CAMERA_PURCHASE_CREDITS); - habbo.givePoints(CAMERA_PURCHASE_POINTS_TYPE, -CAMERA_PURCHASE_POINTS); - AchievementManager.progressAchievement(habbo, Emulator.getGameEnvironment().getAchievementManager().getAchievement("CameraPhotoCount")); - } - } -} diff --git a/Plugins/Camera/src/main/java/com/eu/camera/handlers/RenderRoomEvent.java b/Plugins/Camera/src/main/java/com/eu/camera/handlers/RenderRoomEvent.java deleted file mode 100644 index 38ec10ce..00000000 --- a/Plugins/Camera/src/main/java/com/eu/camera/handlers/RenderRoomEvent.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.eu.camera.handlers; - -import com.eu.habbo.Emulator; -import com.eu.habbo.messages.incoming.MessageHandler; -import com.eu.habbo.habbohotel.rooms.Room; -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.messages.outgoing.camera.CameraURLComposer; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufInputStream; -import io.netty.buffer.ByteBufUtil; - -import javax.imageio.ImageIO; -import java.awt.Graphics2D; -import java.awt.Image; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; - -public class RenderRoomEvent extends MessageHandler { - public static final int CAMERA_RENDER_DELAY = Emulator.getConfig().getInt("camera.render.delay", 5); - - private ByteBuf image = null; - - @Override - public void handle() { - try { - make(); - } finally { - if (this.image != null) { - this.image.release(); - } - } - } - - private void make() { - Habbo habbo = this.client.getHabbo(); - if (!habbo.hasPermission("acc_camera")) { - habbo.alert(Emulator.getTexts().getValue("camera.permission")); - return; - } - - HabboInfo habboInfo = habbo.getHabboInfo(); - HabboStats habboStats = habbo.getHabboStats(); - - int timestamp = Emulator.getIntUnixTimestamp(); - - if (habboStats.cache.containsKey("camera_render_cooldown")) { - int cameraTimestamp = (int) habboStats.cache.get("camera_render_cooldown"); - if (timestamp - cameraTimestamp < CAMERA_RENDER_DELAY) { - String alertMessage = Emulator.getTexts().getValue("camera.wait") - .replace("%seconds%", Integer.toString(timestamp - cameraTimestamp)); - habbo.alert(alertMessage); - - // Show the correct last photo. - String[] splittedPhotoURL = habboInfo.getPhotoURL().split("/"); - if (splittedPhotoURL.length > 0) { - this.client.sendResponse(new CameraURLComposer(splittedPhotoURL[splittedPhotoURL.length - 1])); - } - - return; - } - } - habboStats.cache.put("camera_render_cooldown", (Object) timestamp); - - Room room = habboInfo.getCurrentRoom(); - if (room == null) { - return; - } - - final int count = this.packet.readInt(); - this.image = this.packet.getBuffer().readBytes(count); - if (this.image == null) { - return; - } - - // Prevent possible exploits. - byte[] imageBytes = ByteBufUtil.getBytes(this.image, 0, 4, true); - if (imageBytes == null || imageBytes.length < 4 || !isPNG(imageBytes)) { - return; - } - - BufferedImage theImage; - try (ByteBufInputStream in = new ByteBufInputStream(this.image)) { - theImage = ImageIO.read(in); - } catch (IOException e) { - handleImageProcessingError(habbo); - return; - } - - String fileName = habboInfo.getId() + "_" + timestamp; - String URL = fileName + ".png"; - String URLsmall = fileName + "_small.png"; - String base = Emulator.getConfig().getValue("camera.url"); - - String json = Emulator.getConfig().getValue("camera.extradata") - .replace("%timestamp%", Integer.toString(timestamp)) - .replace("%room_id%", Integer.toString(room.getId())) - .replace("%url%", base + URL); - - habboInfo.setPhotoURL(base + URL); - habboInfo.setPhotoTimestamp(timestamp); - habboInfo.setPhotoRoomId(room.getId()); - habboInfo.setPhotoJSON(json); - - File imageFile = new File(Emulator.getConfig().getValue("imager.location.output.camera") + URL); - File smallImageFile = new File(Emulator.getConfig().getValue("imager.location.output.camera") + URLsmall); - - try { - ImageIO.write(theImage, "png", imageFile); - - Image smallImage = theImage.getScaledInstance(theImage.getWidth(null) / 2, theImage.getHeight(null) / 2, Image.SCALE_SMOOTH); - - BufferedImage bi = new BufferedImage(smallImage.getWidth(null), smallImage.getHeight(null), BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics2D = bi.createGraphics(); - graphics2D.drawImage(smallImage, 0, 0, null); - graphics2D.dispose(); - - ImageIO.write(bi, "png", smallImageFile); - } catch (IOException e) { - handleImageProcessingError(habbo); - return; - } - - this.client.sendResponse(new CameraURLComposer(URL)); - } - - private boolean isPNG(byte[] bytes) { - return bytes[0] == -119 && bytes[1] == 80 && bytes[2] == 78 && bytes[3] == 71; - } - - private void handleImageProcessingError(Habbo habbo) { - habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); - } -} diff --git a/Plugins/Camera/src/main/java/com/eu/camera/handlers/RenderRoomThumbnailEvent.java b/Plugins/Camera/src/main/java/com/eu/camera/handlers/RenderRoomThumbnailEvent.java deleted file mode 100644 index 787a445c..00000000 --- a/Plugins/Camera/src/main/java/com/eu/camera/handlers/RenderRoomThumbnailEvent.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.eu.camera.handlers; - -import com.eu.habbo.Emulator; -import com.eu.habbo.messages.incoming.MessageHandler; -import com.eu.habbo.habbohotel.rooms.Room; -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.messages.outgoing.camera.CameraRoomThumbnailSavedComposer; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufInputStream; -import io.netty.buffer.ByteBufUtil; -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; - -public class RenderRoomThumbnailEvent extends MessageHandler { - public static final int CAMERA_RENDER_DELAY = Emulator.getConfig().getInt("camera.render.delay", 5); - - private ByteBuf image = null; - - @Override - public void handle() { - try { - make(); - } finally { - if (this.image != null) { - this.image.release(); - } - } - } - - private void make() { - Habbo habbo = this.client.getHabbo(); - if (!habbo.hasPermission("acc_camera")) { - habbo.alert(Emulator.getTexts().getValue("camera.permission")); - return; - } - - HabboStats habboStats = habbo.getHabboStats(); - - int timestamp = Emulator.getIntUnixTimestamp(); - - if (habboStats.cache.containsKey("camera_render_cooldown")) { - int cameraTimestamp = (int) habboStats.cache.get("camera_render_cooldown"); - if (timestamp - cameraTimestamp < CAMERA_RENDER_DELAY) { - String alertMessage = Emulator.getTexts().getValue("camera.wait") - .replace("%seconds%", Integer.toString(timestamp - cameraTimestamp)); - habbo.alert(alertMessage); - return; - } - } - habboStats.cache.put("camera_render_cooldown", (Object) timestamp); - - HabboInfo habboInfo = habbo.getHabboInfo(); - - Room room = habboInfo.getCurrentRoom(); - if (room == null || !room.isOwner(habbo)) { - return; - } - - final int count = this.packet.readInt(); - this.image = this.packet.getBuffer().readBytes(count); - if (this.image == null || !isValidImage(this.image)) { - return; - } - - BufferedImage theImage = null; - try (ByteBufInputStream in = new ByteBufInputStream(this.image)) { - theImage = ImageIO.read(in); - } catch (IOException e) { - e.printStackTrace(); - habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); - return; - } - - File imageFile = new File(Emulator.getConfig().getValue("imager.location.output.thumbnail") + room.getId() + ".png"); - - try { - ImageIO.write(theImage, "png", imageFile); - } catch (IOException e) { - e.printStackTrace(); - habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); - return; - } - - this.client.sendResponse(new CameraRoomThumbnailSavedComposer()); - } - - private boolean isValidImage(ByteBuf imageBuffer) { - byte[] imageBytes = ByteBufUtil.getBytes(imageBuffer, 0, 4, true); - return (imageBytes != null && imageBytes.length >= 4 && - imageBytes[0] == -119 && imageBytes[1] == 80 && imageBytes[2] == 78 && imageBytes[3] == 71); - } -} diff --git a/Plugins/Camera/src/main/resources/plugin.json b/Plugins/Camera/src/main/resources/plugin.json deleted file mode 100644 index c5b257ed..00000000 --- a/Plugins/Camera/src/main/resources/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "main" : "com.eu.camera.Main", - "name" : "Camera", - "author" : "EntenKoeniq and habbo.sx" -} - \ No newline at end of file diff --git a/Plugins/Javascript-Plugin/.idea/compiler.xml b/Plugins/Javascript-Plugin/.idea/compiler.xml deleted file mode 100644 index 4ce00136..00000000 --- a/Plugins/Javascript-Plugin/.idea/compiler.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/Plugins/Javascript-Plugin/.idea/jarRepositories.xml b/Plugins/Javascript-Plugin/.idea/jarRepositories.xml deleted file mode 100644 index 712ab9d9..00000000 --- a/Plugins/Javascript-Plugin/.idea/jarRepositories.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Plugins/Javascript-Plugin/.idea/misc.xml b/Plugins/Javascript-Plugin/.idea/misc.xml deleted file mode 100644 index 44185bb9..00000000 --- a/Plugins/Javascript-Plugin/.idea/misc.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Plugins/Javascript-Plugin/.idea/workspace.xml b/Plugins/Javascript-Plugin/.idea/workspace.xml deleted file mode 100644 index b1aedd92..00000000 --- a/Plugins/Javascript-Plugin/.idea/workspace.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - 1712556687432 - - - - \ No newline at end of file diff --git a/Plugins/Javascript-Plugin/README.md b/Plugins/Javascript-Plugin/README.md deleted file mode 100644 index 25fdcd7e..00000000 --- a/Plugins/Javascript-Plugin/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# javascript-plugin -Allows 2-way communication between some javascript running in your client and your emulator using the FlashExternalInterface - -## How do I use it? -You can use the following Vue application which will work out of the box with this plugin: https://github.com/dank074/youtube-overlay -Just include the scripts and add a div with id=app, and you are set to go - -## I don't wanna use that Vue trash, how do I use it with my own javascript? - -Receiving a message: -```js -FlashExternalInterface.openHabblet = function(a,b){console.log("recieved " + a)} -``` - -Sending a message: -```js -document.querySelector('object, embed').openroom(JSON.stringify({"header": "test", "data": {"name": "Efrain"}})) -``` -## Built in features -- [x] Arrowkey walking -- [x] Youtube jukebox (use interaction `yt_jukebox` on any furniture) -- [x] Slots Machine (use interaction `slots_machine` on any furniture) -- [x] :youtube command ```ALTER TABLE permissions - ADD COLUMN cmd_youtube enum('0','1') DEFAULT '1';``` diff --git a/Plugins/Javascript-Plugin/compiled/Javascript-Plugin-1.1-SNAPSHOT.jar b/Plugins/Javascript-Plugin/compiled/Javascript-Plugin-1.1-SNAPSHOT.jar deleted file mode 100644 index 9e5595df..00000000 Binary files a/Plugins/Javascript-Plugin/compiled/Javascript-Plugin-1.1-SNAPSHOT.jar and /dev/null differ diff --git a/Plugins/Javascript-Plugin/pom.xml b/Plugins/Javascript-Plugin/pom.xml deleted file mode 100644 index 2fd7c0e0..00000000 --- a/Plugins/Javascript-Plugin/pom.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - 4.0.0 - - com.skeletor - Javascript-Plugin - 1.1-SNAPSHOT - - - - org.apache.maven.plugins - maven-compiler-plugin - - 8 - 8 - - - - - - - - com.eu.habbo - Habbo - 3.5.1 - - - - com.googlecode.owasp-java-html-sanitizer - owasp-java-html-sanitizer - 20240325.1 - compile - - - - - \ No newline at end of file diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/audio/RoomAudioManager.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/audio/RoomAudioManager.java deleted file mode 100644 index 6714f3bd..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/audio/RoomAudioManager.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.skeletor.plugin.javascript.audio; - -import java.util.concurrent.ConcurrentHashMap; - -public class RoomAudioManager { - private static RoomAudioManager _instance; - - private ConcurrentHashMap roomAudio = new ConcurrentHashMap<>(); - - public RoomPlaylist getPlaylistForRoom(int roomId) { - if (this.roomAudio.containsKey(Integer.valueOf(roomId))) - return this.roomAudio.get(Integer.valueOf(roomId)); - RoomPlaylist newPlaylist = new RoomPlaylist(); - this.roomAudio.put(Integer.valueOf(roomId), newPlaylist); - return newPlaylist; - } - - public void dispose(int roomId) { - this.roomAudio.remove(Integer.valueOf(roomId)); - } - - public static void Init() { - _instance = new RoomAudioManager(); - } - - public void Dispose() { - this.roomAudio.clear(); - _instance = null; - } - - public static RoomAudioManager getInstance() { - if (_instance == null) - _instance = new RoomAudioManager(); - return _instance; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/audio/RoomPlaylist.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/audio/RoomPlaylist.java deleted file mode 100644 index 07561066..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/audio/RoomPlaylist.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.skeletor.plugin.javascript.audio; - -import com.eu.habbo.messages.outgoing.MessageComposer; -import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; -import gnu.trove.map.hash.THashMap; -import java.util.ArrayList; - -import static com.skeletor.plugin.javascript.utils.RegexUtility.sanitize; - -public class RoomPlaylist { - private ArrayList playlist = new ArrayList<>(); - - private int current = 0; - - private boolean playing = false; - - public YoutubeVideo nextSong() { - if (this.current < this.playlist.size() - 1) { - this.current++; - } else { - this.current = 0; - } - return this.playlist.get(this.current); - } - - public YoutubeVideo prevSong() { - if (this.current > 0) - this.current--; - return this.playlist.get(this.current); - } - - public boolean isPlaying() { - return this.playing; - } - - public void setPlaying(boolean playing) { - this.playing = playing; - } - - public void addSong(YoutubeVideo song) { - this.playlist.add(song); - } - - public YoutubeVideo removeSong(int index) { - YoutubeVideo res = null; - if(playlist.size() - 1 >= index) - res = this.playlist.remove(index); - if(playlist.isEmpty()) this.setPlaying(false); - if(index == this.getCurrentIndex()) { - if(index > this.playlist.size() - 1 && !this.playlist.isEmpty()) { - this.current = this.playlist.size() - 1; - } - } - else if(index < this.getCurrentIndex() && this.getCurrentIndex() > 0) { - this.current--; - } - return res; - } - - public YoutubeVideo getCurrentSong() { - return this.playlist.get(this.current); - } - - public int getCurrentIndex() { - return this.current; - } - - public ArrayList getPlaylist() { - return this.playlist; - } - - public static class YoutubeVideo { - public String name; - - public String videoId; - - public String channel; - - public YoutubeVideo(String name, String videoId, String channel) { - this.name = name; - this.videoId = videoId; - this.channel = channel; - } - } - - public MessageComposer getNowPlayingBubbleAlert() { - final THashMap keys = new THashMap<>(); - keys.put("display", "BUBBLE"); - keys.put("image", ("${image.library.url}notifications/music.png")); - keys.put("message", "Now playing " + sanitize(this.getCurrentSong().name)); - return new BubbleAlertComposer("", keys); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/categories/Category.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/categories/Category.java deleted file mode 100644 index 0629cf5b..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/categories/Category.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.skeletor.plugin.javascript.categories; - -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; - -public class Category { - private int id; - - private String name; - - private List permissions; - - public Category(ResultSet set) throws SQLException { - this.id = set.getInt("id"); - this.name = set.getString("name"); - this.permissions = new ArrayList<>(); - } - - public void addPermission(String permission) { - if (!this.permissions.contains(permission)) - this.permissions.add(permission); - } - - public int getId() { - return this.id; - } - - public String getName() { - return this.name; - } - - public List getPermissions() { - return this.permissions; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/categories/CommandManager.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/categories/CommandManager.java deleted file mode 100644 index e45ecf93..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/categories/CommandManager.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.skeletor.plugin.javascript.categories; - -import com.eu.habbo.Emulator; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CommandManager { - private static final Logger LOGGER = LoggerFactory.getLogger(CommandManager.class); - - private final List commandCategories; - - public CommandManager() { - this.commandCategories = new ArrayList<>(); - reload(); - } - - public void reload() { - dispose(); - try(Connection connection = Emulator.getDatabase().getDataSource().getConnection(); - PreparedStatement statement = connection.prepareStatement("SELECT * FROM `command_categories` ORDER BY `order`"); - ResultSet set = statement.executeQuery()) { - while (set.next()) { - Category category = new Category(set); - this.commandCategories.add(category); - } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } - try(Connection connection = Emulator.getDatabase().getDataSource().getConnection(); - PreparedStatement statement = connection.prepareStatement("SELECT * FROM `command_category_permissions` ORDER BY `order`"); - ResultSet set = statement.executeQuery()) { - while (set.next()) { - if (hasCommandCategory(Integer.valueOf(set.getInt("category_id")))) { - Category category = getCommandCategory(Integer.valueOf(set.getInt("category_id"))); - ((Category)this.commandCategories.get(this.commandCategories.indexOf(category))).addPermission(set.getString("permission")); - } - } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } - } - - public void dispose() { - this.commandCategories.clear(); - } - - public boolean hasCommandCategory(Integer id) { - return - - (((List)this.commandCategories.stream().filter(p -> (p.getId() == id.intValue())).collect(Collectors.toList())).size() > 0); - } - - public Category getCommandCategory(Integer id) { - if (hasCommandCategory(id)) - return ((List)this.commandCategories - .stream() - .filter(p -> (p.getId() == id.intValue())) - .collect(Collectors.toList())).get(0); - return null; - } - - public List getCommandCategories() { - return this.commandCategories; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/commands/CmdCommand.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/commands/CmdCommand.java deleted file mode 100644 index 21e29688..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/commands/CmdCommand.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.skeletor.plugin.javascript.commands; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.commands.Command; -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.messages.outgoing.MessageComposer; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.common.CommandsComposer; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; -import java.util.List; - -public class CmdCommand extends Command { - public CmdCommand() { - super("cmd_commands", Emulator.getTexts().getValue("jscommands.keys.cmd_commands").split(";")); - } - - public boolean handle(GameClient gameClient, String[] strings) throws Exception { - List commands = Emulator.getGameEnvironment().getCommandHandler().getCommandsForRank(gameClient.getHabbo().getHabboInfo().getRank().getId()); - CommandsComposer commandsComposer = new CommandsComposer(commands); - gameClient.sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)commandsComposer)); - return true; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/commands/TwitchCommand.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/commands/TwitchCommand.java deleted file mode 100644 index 0cdd2096..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/commands/TwitchCommand.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.skeletor.plugin.javascript.commands; - -import com.eu.habbo.habbohotel.commands.Command; -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.habbohotel.rooms.Room; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.common.TwitchVideoComposer; -import com.skeletor.plugin.javascript.main; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; - -public class TwitchCommand extends Command { - public TwitchCommand() { - super("cmd_twitch", new String[] { "twitch" }); - } - - public boolean handle(GameClient gameClient, String[] strings) throws Exception { - Room room = gameClient.getHabbo().getHabboInfo().getCurrentRoom(); - if ((room.hasRights(gameClient.getHabbo()) || gameClient.getHabbo().getHabboInfo().getRank().getName().equals("VIP")) && - strings.length > 1) { - String videoId = strings[1]; - if (videoId.isEmpty()) { - gameClient.getHabbo().whisper("You must supply the twitch channel/video"); - return true; - } - main.addTwitchRoom(room.getId(), videoId); - TwitchVideoComposer twitchVideoComposer = new TwitchVideoComposer(videoId); - room.sendComposer((new JavascriptCallbackComposer((OutgoingWebMessage)twitchVideoComposer)).compose()); - return true; - } - gameClient.getHabbo().whisper("You do not have permission to use this command in this room"); - return true; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/commands/UpdateCategoriesCommand.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/commands/UpdateCategoriesCommand.java deleted file mode 100644 index f44bf3a2..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/commands/UpdateCategoriesCommand.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.skeletor.plugin.javascript.commands; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.commands.Command; -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.plugin.EventListener; -import com.skeletor.plugin.javascript.main; - -public class UpdateCategoriesCommand extends Command implements EventListener { - public UpdateCategoriesCommand(String permission, String[] keys) { - super(permission, keys); - } - - public boolean handle(GameClient gameClient, String[] strings) throws Exception { - main.getCommandManager().reload(); - gameClient.getHabbo().whisper(Emulator.getTexts().getValue("categories.cmd_update_categories.success")); - return true; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/commands/YoutubeCommand.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/commands/YoutubeCommand.java deleted file mode 100644 index d509ab57..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/commands/YoutubeCommand.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.skeletor.plugin.javascript.commands; - -import com.eu.habbo.habbohotel.commands.Command; -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.habbohotel.rooms.Room; -import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.common.YoutubeTVComposer; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; -import com.skeletor.plugin.javascript.utils.RegexUtility; - -public class YoutubeCommand extends Command { - public YoutubeCommand() { - super("cmd_youtube", new String[] { "youtube" }); - } - - public boolean handle(GameClient gameClient, String[] strings) throws Exception { - Room room = gameClient.getHabbo().getHabboInfo().getCurrentRoom(); - if (strings.length > 1) { - String videoId = RegexUtility.getYouTubeId(strings[1]); - int time = 0; - if (videoId.isEmpty()) { - gameClient.getHabbo().whisper("Invalid youtube url", RoomChatMessageBubbles.ALERT); - return true; - } - if (strings[1].contains("t=")) - try { - String[] realParams = strings[1].split("\\?"); - if (realParams.length > 1) { - String[] params = realParams[1].split("&"); - for (String param : params) { - String[] split = param.split("="); - if (split.length > 1 && - split[0].equals("t")) - time = Integer.parseInt(split[1].replace("s", "")); - } - } - } catch (Exception exception) {} - YoutubeTVComposer youtubeTVComposer = new YoutubeTVComposer(videoId, Integer.valueOf(time)); - room.sendComposer((new JavascriptCallbackComposer((OutgoingWebMessage)youtubeTVComposer)).compose()); - return true; - } - return true; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/CommunicationManager.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/CommunicationManager.java deleted file mode 100644 index 77772c62..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/CommunicationManager.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.skeletor.plugin.javascript.communication; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.skeletor.plugin.javascript.communication.incoming.IncomingWebMessage; -import com.skeletor.plugin.javascript.communication.incoming.audio.AddSongEvent; -import com.skeletor.plugin.javascript.communication.incoming.audio.NextSongEvent; -import com.skeletor.plugin.javascript.communication.incoming.audio.PlayStopEvent; -import com.skeletor.plugin.javascript.communication.incoming.audio.PreviousSongEvent; -import com.skeletor.plugin.javascript.communication.incoming.audio.RemoveSongEvent; -import com.skeletor.plugin.javascript.communication.incoming.audio.SongEndedEvent; -import com.skeletor.plugin.javascript.communication.incoming.common.MoveAvatarEvent; -import com.skeletor.plugin.javascript.communication.incoming.common.RequestCommandsEvent; -import com.skeletor.plugin.javascript.communication.incoming.common.RequestCreditsEvent; -import com.skeletor.plugin.javascript.communication.incoming.common.RequestSpinSlotMachineEvent; -import com.skeletor.plugin.javascript.communication.incoming.loaded.LoadedEvent; -import com.skeletor.plugin.javascript.utils.JsonFactory; -import gnu.trove.map.hash.THashMap; - -public class CommunicationManager { - private static CommunicationManager instance; - - private final THashMap> _incomingMessages; - - static { - try { - instance = new CommunicationManager(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public CommunicationManager() { - this._incomingMessages = new THashMap(); - initializeMessages(); - } - - public void initializeMessages() { - registerMessage("move_avatar", (Class)MoveAvatarEvent.class); - registerMessage("request_credits", (Class)RequestCreditsEvent.class); - registerMessage("spin_slot_machine", (Class)RequestSpinSlotMachineEvent.class); - registerMessage("add_song", (Class)AddSongEvent.class); - registerMessage("next_song", (Class)NextSongEvent.class); - registerMessage("prev_song", (Class)PreviousSongEvent.class); - registerMessage("play_stop", (Class)PlayStopEvent.class); - registerMessage("remove_song", (Class)RemoveSongEvent.class); - registerMessage("song_ended", (Class)SongEndedEvent.class); - registerMessage("request_commands", (Class)RequestCommandsEvent.class); - registerMessage("js_loaded", (Class)LoadedEvent.class); - } - - public void registerMessage(String key, Class message) { - this._incomingMessages.put(key, message); - } - - public THashMap> getIncomingMessages() { - return this._incomingMessages; - } - - public static CommunicationManager getInstance() { - if (instance == null) - try { - instance = new CommunicationManager(); - } catch (Exception e) { - Emulator.getLogging().logErrorLine(e.getMessage()); - } - return instance; - } - - public void OnMessage(String jsonPayload, GameClient sender) { - try { - IncomingWebMessage.JSONIncomingEvent heading = (IncomingWebMessage.JSONIncomingEvent)JsonFactory.getInstance().fromJson(jsonPayload, IncomingWebMessage.JSONIncomingEvent.class); - Class message = (Class)getInstance().getIncomingMessages().get(heading.header); - IncomingWebMessage webEvent = message.getDeclaredConstructor(new Class[0]).newInstance(new Object[0]); - webEvent.handle(sender, JsonFactory.getInstance().fromJson(heading.data.toString(), webEvent.type)); - } catch (Exception e) { - Emulator.getLogging().logUndefinedPacketLine("unknown message: " + jsonPayload); - } - } - - public void Dispose() { - this._incomingMessages.clear(); - instance = null; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/IncomingWebMessage.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/IncomingWebMessage.java deleted file mode 100644 index add0095d..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/IncomingWebMessage.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.skeletor.plugin.javascript.communication.incoming; - -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.google.gson.JsonObject; - -public abstract class IncomingWebMessage { - public final Class type; - - public IncomingWebMessage(Class type) { - this.type = type; - } - - public abstract void handle(GameClient paramGameClient, T paramT); - - public static class JSONIncomingEvent { - public String header; - - public JsonObject data; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/AddSongEvent.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/AddSongEvent.java deleted file mode 100644 index 0890434b..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/AddSongEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.skeletor.plugin.javascript.communication.incoming.audio; - -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.habbohotel.rooms.Room; -import com.skeletor.plugin.javascript.audio.RoomAudioManager; -import com.skeletor.plugin.javascript.audio.RoomPlaylist; -import com.skeletor.plugin.javascript.communication.incoming.IncomingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.audio.AddSongComposer; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; - -public class AddSongEvent extends IncomingWebMessage { - public AddSongEvent() { - super(JSONAddSong.class); - } - - public void handle(GameClient client, JSONAddSong message) { - Room currentRoom = client.getHabbo().getHabboInfo().getCurrentRoom(); - if (currentRoom == null) - return; - if (currentRoom.hasRights(client.getHabbo())) { - RoomPlaylist playlist = RoomAudioManager.getInstance().getPlaylistForRoom(currentRoom.getId()); - RoomPlaylist.YoutubeVideo song = new RoomPlaylist.YoutubeVideo(message.name, message.videoId, message.channel); - playlist.addSong(song); - AddSongComposer addSongComposer = new AddSongComposer(song); - currentRoom.sendComposer((new JavascriptCallbackComposer((OutgoingWebMessage)addSongComposer)).compose()); - } - } - - public static class JSONAddSong { - public String name; - - public String videoId; - - public String channel; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/NextSongEvent.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/NextSongEvent.java deleted file mode 100644 index c6b5c416..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/NextSongEvent.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.skeletor.plugin.javascript.communication.incoming.audio; - -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.habbohotel.rooms.Room; -import com.skeletor.plugin.javascript.audio.RoomAudioManager; -import com.skeletor.plugin.javascript.audio.RoomPlaylist; -import com.skeletor.plugin.javascript.communication.incoming.IncomingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.audio.PlaySongComposer; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; - -public class NextSongEvent extends IncomingWebMessage { - public NextSongEvent() { - super(JSONNextSongEvent.class); - } - - public void handle(GameClient client, JSONNextSongEvent message) { - Room currentRoom = client.getHabbo().getHabboInfo().getCurrentRoom(); - if (currentRoom == null) - return; - if (currentRoom.hasRights(client.getHabbo())) { - RoomPlaylist playlist = RoomAudioManager.getInstance().getPlaylistForRoom(currentRoom.getId()); - playlist.nextSong(); - playlist.setPlaying(true); - PlaySongComposer playSongComposer = new PlaySongComposer(playlist.getCurrentIndex()); - currentRoom.sendComposer((new JavascriptCallbackComposer((OutgoingWebMessage)playSongComposer)).compose()); - currentRoom.sendComposer(playlist.getNowPlayingBubbleAlert().compose()); - } - } - - public static class JSONNextSongEvent { - public int currentIndex; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/PlayStopEvent.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/PlayStopEvent.java deleted file mode 100644 index 646a7492..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/PlayStopEvent.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.skeletor.plugin.javascript.communication.incoming.audio; - -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.habbohotel.rooms.Room; -import com.skeletor.plugin.javascript.audio.RoomAudioManager; -import com.skeletor.plugin.javascript.audio.RoomPlaylist; -import com.skeletor.plugin.javascript.communication.incoming.IncomingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.audio.PlayStopComposer; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; - -public class PlayStopEvent extends IncomingWebMessage { - public PlayStopEvent() { - super(JSONPlayStopEvent.class); - } - - public void handle(GameClient client, JSONPlayStopEvent message) { - Room room = client.getHabbo().getHabboInfo().getCurrentRoom(); - if (room == null) - return; - if (room.hasRights(client.getHabbo())) { - RoomPlaylist roomPlaylist = RoomAudioManager.getInstance().getPlaylistForRoom(room.getId()); - roomPlaylist.setPlaying(message.play); - room.sendComposer((new JavascriptCallbackComposer((OutgoingWebMessage)new PlayStopComposer(message.play))).compose()); - if (message.play) - room.sendComposer(roomPlaylist.getNowPlayingBubbleAlert().compose()); - } - } - - public static class JSONPlayStopEvent { - public boolean play; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/PreviousSongEvent.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/PreviousSongEvent.java deleted file mode 100644 index b443c473..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/PreviousSongEvent.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.skeletor.plugin.javascript.communication.incoming.audio; - -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.habbohotel.rooms.Room; -import com.skeletor.plugin.javascript.audio.RoomAudioManager; -import com.skeletor.plugin.javascript.audio.RoomPlaylist; -import com.skeletor.plugin.javascript.communication.incoming.IncomingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.audio.PlaySongComposer; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; - -public class PreviousSongEvent extends IncomingWebMessage { - public PreviousSongEvent() { - super(JSONPreviousSongEvent.class); - } - - public void handle(GameClient client, JSONPreviousSongEvent message) { - Room currentRoom = client.getHabbo().getHabboInfo().getCurrentRoom(); - if (currentRoom == null) - return; - if (currentRoom.hasRights(client.getHabbo())) { - RoomPlaylist playlist = RoomAudioManager.getInstance().getPlaylistForRoom(currentRoom.getId()); - playlist.prevSong(); - playlist.setPlaying(true); - PlaySongComposer playSongComposer = new PlaySongComposer(playlist.getCurrentIndex()); - currentRoom.sendComposer((new JavascriptCallbackComposer((OutgoingWebMessage)playSongComposer)).compose()); - currentRoom.sendComposer(playlist.getNowPlayingBubbleAlert().compose()); - } - } - - public static class JSONPreviousSongEvent { - public int currentIndex; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/RemoveSongEvent.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/RemoveSongEvent.java deleted file mode 100644 index 4dde4ee6..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/RemoveSongEvent.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.skeletor.plugin.javascript.communication.incoming.audio; - -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.habbohotel.rooms.Room; -import com.skeletor.plugin.javascript.audio.RoomAudioManager; -import com.skeletor.plugin.javascript.audio.RoomPlaylist; -import com.skeletor.plugin.javascript.communication.incoming.IncomingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.audio.RemoveSongComposer; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; - -public class RemoveSongEvent extends IncomingWebMessage { - public RemoveSongEvent() { - super(JSONRemoveSongEvent.class); - } - - public void handle(GameClient client, JSONRemoveSongEvent message) { - Room room = client.getHabbo().getHabboInfo().getCurrentRoom(); - if (room == null) - return; - if (room.hasRights(client.getHabbo())) { - RoomPlaylist roomPlaylist = RoomAudioManager.getInstance().getPlaylistForRoom(room.getId()); - roomPlaylist.removeSong(message.index); - room.sendComposer((new JavascriptCallbackComposer((OutgoingWebMessage)new RemoveSongComposer(message.index))).compose()); - } - } - - public static class JSONRemoveSongEvent { - public int index; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/SongEndedEvent.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/SongEndedEvent.java deleted file mode 100644 index 499e1def..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/audio/SongEndedEvent.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.skeletor.plugin.javascript.communication.incoming.audio; - -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.habbohotel.rooms.Room; -import com.skeletor.plugin.javascript.audio.RoomAudioManager; -import com.skeletor.plugin.javascript.audio.RoomPlaylist; -import com.skeletor.plugin.javascript.communication.incoming.IncomingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.audio.PlaySongComposer; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; - -public class SongEndedEvent extends IncomingWebMessage { - public SongEndedEvent() { - super(JSONSongEndedEvent.class); - } - - public void handle(GameClient client, JSONSongEndedEvent message) { - Room room = client.getHabbo().getHabboInfo().getCurrentRoom(); - if (room == null) - return; - if (room.hasRights(client.getHabbo())) { - RoomPlaylist playlist = RoomAudioManager.getInstance().getPlaylistForRoom(room.getId()); - if (playlist.getCurrentIndex() == message.currentIndex) { - playlist.nextSong(); - playlist.setPlaying(true); - PlaySongComposer playSongComposer = new PlaySongComposer(playlist.getCurrentIndex()); - room.sendComposer((new JavascriptCallbackComposer((OutgoingWebMessage)playSongComposer)).compose()); - room.sendComposer(playlist.getNowPlayingBubbleAlert().compose()); - } - } - } - - class JSONSongEndedEvent { - public int currentIndex; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/common/MoveAvatarEvent.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/common/MoveAvatarEvent.java deleted file mode 100644 index 8db59e41..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/common/MoveAvatarEvent.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.skeletor.plugin.javascript.communication.incoming.common; - -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.habbohotel.rooms.Room; -import com.eu.habbo.habbohotel.rooms.RoomTile; -import com.skeletor.plugin.javascript.communication.incoming.IncomingWebMessage; - -public class MoveAvatarEvent extends IncomingWebMessage { - private static final short DEFAULT_WALK_AMOUNT = 1; - - public MoveAvatarEvent() { - super(JSONMoveAvatarEvent.class); - } - - public void handle(GameClient client, JSONMoveAvatarEvent message) { - Room room = client.getHabbo().getRoomUnit().getRoom(); - if (room == null) - return; - short x = (client.getHabbo().getRoomUnit().getGoal()).x; - short y = (client.getHabbo().getRoomUnit().getGoal()).y; - switch (message.direction) { - case "stop": - return; - case "left": - y = (short)(y + 1); - break; - case "right": - y = (short)(y - 1); - break; - case "up": - x = (short)(x - 1); - break; - case "down": - x = (short)(x + 1); - break; - default: - return; - } - try { - RoomTile goal = room.getLayout().getTile(x, y); - if (goal == null) - return; - if (goal.isWalkable() || client.getHabbo().getHabboInfo().getCurrentRoom().canSitOrLayAt(goal.x, goal.y)) { - if (client.getHabbo().getRoomUnit().getMoveBlockingTask() != null) - client.getHabbo().getRoomUnit().getMoveBlockingTask().get(); - client.getHabbo().getRoomUnit().setGoalLocation(goal); - } - } catch (Exception exception) {} - } - - static class JSONMoveAvatarEvent { - String direction; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/common/RequestCommandsEvent.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/common/RequestCommandsEvent.java deleted file mode 100644 index c67ea48a..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/common/RequestCommandsEvent.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.skeletor.plugin.javascript.communication.incoming.common; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.commands.Command; -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.messages.outgoing.MessageComposer; -import com.skeletor.plugin.javascript.communication.incoming.IncomingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.common.CommandsComposer; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; -import java.util.List; - -public class RequestCommandsEvent extends IncomingWebMessage { - public RequestCommandsEvent() { - super(JSONRequestCommandsEvent.class); - } - - public void handle(GameClient client, JSONRequestCommandsEvent message) { - List commands = Emulator.getGameEnvironment().getCommandHandler().getCommandsForRank(client.getHabbo().getHabboInfo().getRank().getId()); - CommandsComposer commandsComposer = new CommandsComposer(commands); - client.sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)commandsComposer)); - } - - static class JSONRequestCommandsEvent { - boolean idk; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/common/RequestCreditsEvent.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/common/RequestCreditsEvent.java deleted file mode 100644 index 9c8452ae..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/common/RequestCreditsEvent.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.skeletor.plugin.javascript.communication.incoming.common; - -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.messages.outgoing.MessageComposer; -import com.eu.habbo.messages.outgoing.users.UserCreditsComposer; -import com.skeletor.plugin.javascript.communication.incoming.IncomingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.common.UpdateCreditsComposer; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; - -public class RequestCreditsEvent extends IncomingWebMessage { - public RequestCreditsEvent() { - super(JSONRequestCreditsEvent.class); - } - - public void handle(GameClient client, JSONRequestCreditsEvent message) { - client.sendResponse((MessageComposer)new UserCreditsComposer(client.getHabbo())); - UpdateCreditsComposer creditsComposer = new UpdateCreditsComposer(client.getHabbo().getHabboInfo().getCredits()); - client.sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)creditsComposer)); - } - - static class JSONRequestCreditsEvent { - boolean idk; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/common/RequestSpinSlotMachineEvent.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/common/RequestSpinSlotMachineEvent.java deleted file mode 100644 index 58ea5a86..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/common/RequestSpinSlotMachineEvent.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.skeletor.plugin.javascript.communication.incoming.common; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles; -import com.eu.habbo.habbohotel.users.HabboItem; -import com.eu.habbo.messages.outgoing.MessageComposer; -import com.eu.habbo.messages.outgoing.users.UserCreditsComposer; -import com.skeletor.plugin.javascript.communication.incoming.IncomingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.common.UpdateCreditsComposer; -import com.skeletor.plugin.javascript.communication.outgoing.slotmachine.SpinResultComposer; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; - -public class RequestSpinSlotMachineEvent extends IncomingWebMessage { - private static final int maxNumber = 5; - - private static final int LEMON = 0; - - private static final int MELON = 1; - - private static final int GRAPES = 2; - - private static final int CHERRY = 3; - - private static final int BAR = 4; - - public RequestSpinSlotMachineEvent() { - super(JSONRequestSpinSlotMachineEvent.class); - } - - public void handle(GameClient client, JSONRequestSpinSlotMachineEvent message) { - HabboItem item = client.getHabbo().getRoomUnit().getRoom().getHabboItem(message.itemId); - if (item == null) - return; - if (message.bet <= 0 || message.bet > client.getHabbo().getHabboInfo().getCredits()) - return; - client.getHabbo().getHabboInfo().addCredits(-message.bet); - client.sendResponse((MessageComposer)new UserCreditsComposer(client.getHabbo())); - client.getHabbo().talk(Emulator.getTexts().getValue("slot.machines.spin", "* Bets %amount% on Slots Machine *").replace("%amount%", message.bet + ""), RoomChatMessageBubbles.ALERT); - client.sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)new UpdateCreditsComposer(client.getHabbo().getHabboInfo().getCredits()))); - int result1 = Emulator.getRandom().nextInt(5); - int result2 = Emulator.getRandom().nextInt(5); - int result3 = Emulator.getRandom().nextInt(5); - int amountWon = 0; - boolean won = false; - if (result1 == result2 && result2 == result3) { - won = true; - switch (result1) { - case 0: - amountWon = 5 * message.bet; - break; - case 1: - amountWon = 6 * message.bet; - break; - case 2: - amountWon = 10 * message.bet; - break; - case 3: - amountWon = 15 * message.bet; - break; - case 4: - amountWon = 20 * message.bet; - break; - } - client.getHabbo().getHabboInfo().addCredits(amountWon); - } else if (result1 == 4 && result2 == 4) { - won = true; - amountWon = 4 * message.bet; - client.getHabbo().getHabboInfo().addCredits(amountWon); - } else if (result1 == 3 && result2 == 3) { - won = true; - amountWon = 3 * message.bet; - client.getHabbo().getHabboInfo().addCredits(amountWon); - } else if (result1 == 3) { - won = true; - amountWon = 2 * message.bet; - client.getHabbo().getHabboInfo().addCredits(amountWon); - } - SpinResultComposer resultComposer = new SpinResultComposer(result1, result2, result3, won, amountWon); - client.sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)resultComposer)); - int finalAmount = amountWon; - Emulator.getThreading().run(() -> client.getHabbo().talk(Emulator.getTexts().getValue("slot.machines.won", "* Won %amount% in Slots Machine *").replace("%amount%", finalAmount + ""), RoomChatMessageBubbles.ALERT), 5000L); - } - - static class JSONRequestSpinSlotMachineEvent { - int itemId; - - int bet; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/loaded/LoadedEvent.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/loaded/LoadedEvent.java deleted file mode 100644 index ffb47688..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/incoming/loaded/LoadedEvent.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.skeletor.plugin.javascript.communication.incoming.loaded; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.commands.Command; -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.habbohotel.users.Habbo; -import com.eu.habbo.messages.outgoing.MessageComposer; -import com.skeletor.plugin.javascript.communication.incoming.IncomingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.commands.CommandsPopUpComposer; -import com.skeletor.plugin.javascript.main; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; -import java.util.List; - -public class LoadedEvent extends IncomingWebMessage { - public LoadedEvent() { - super(JSONLoadedEvent.class); - } - - public void handle(GameClient client, JSONLoadedEvent message) { - Habbo habbo = client.getHabbo(); - if (habbo == null) - return; - (habbo.getHabboStats()).cache.put(main.USER_LOADED_EVENT, Boolean.valueOf(true)); - List commands = Emulator.getGameEnvironment().getCommandHandler().getCommandsForRank(client.getHabbo().getHabboInfo().getRank().getId()); - CommandsPopUpComposer commandsPopUpComposer = new CommandsPopUpComposer(commands, (habbo.getHabboInfo().getRank().getLevel() >= 5)); - client.sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)commandsPopUpComposer)); - } - - static class JSONLoadedEvent { - boolean idk; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/OutgoingWebMessage.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/OutgoingWebMessage.java deleted file mode 100644 index ce70c591..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/OutgoingWebMessage.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing; - -import com.google.gson.JsonObject; - -public abstract class OutgoingWebMessage { - public String header; - - public JsonObject data; - - public OutgoingWebMessage(String name) { - this.header = name; - this.data = new JsonObject(); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/AddSongComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/AddSongComposer.java deleted file mode 100644 index 7da77f81..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/AddSongComposer.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.audio; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.audio.RoomPlaylist; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class AddSongComposer extends OutgoingWebMessage { - public AddSongComposer(RoomPlaylist.YoutubeVideo video) { - super("add_song"); - JsonObject song = new JsonObject(); - song.add("name", (JsonElement)new JsonPrimitive(video.name)); - song.add("videoId", (JsonElement)new JsonPrimitive(video.videoId)); - song.add("channel", (JsonElement)new JsonPrimitive(video.channel)); - this.data.add("song", (JsonElement)song); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/ChangeVolumeComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/ChangeVolumeComposer.java deleted file mode 100644 index a2080ec0..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/ChangeVolumeComposer.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.audio; - -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class ChangeVolumeComposer extends OutgoingWebMessage { - public ChangeVolumeComposer(int volume) { - super("change_volume"); - this.data.add("volume", (JsonElement)new JsonPrimitive(Integer.valueOf(volume))); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/DisposePlaylistComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/DisposePlaylistComposer.java deleted file mode 100644 index 3b1b7eaa..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/DisposePlaylistComposer.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.audio; - -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class DisposePlaylistComposer extends OutgoingWebMessage { - public DisposePlaylistComposer() { - super("dispose_playlist"); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/JukeboxComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/JukeboxComposer.java deleted file mode 100644 index 15f7fbfb..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/JukeboxComposer.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.audio; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.audio.RoomPlaylist; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class JukeboxComposer extends OutgoingWebMessage { - public JukeboxComposer(RoomPlaylist playlist) { - super("jukebox_player"); - JsonArray playlistJson = new JsonArray(); - for (RoomPlaylist.YoutubeVideo video : playlist.getPlaylist()) { - JsonObject song = new JsonObject(); - song.add("name", (JsonElement)new JsonPrimitive(video.name)); - song.add("videoId", (JsonElement)new JsonPrimitive(video.videoId)); - song.add("channel", (JsonElement)new JsonPrimitive(video.channel)); - playlistJson.add((JsonElement)song); - } - this.data.add("playlist", (JsonElement)playlistJson); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/PlaySongComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/PlaySongComposer.java deleted file mode 100644 index 85ab1dbd..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/PlaySongComposer.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.audio; - -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class PlaySongComposer extends OutgoingWebMessage { - public PlaySongComposer(int index) { - super("play_song"); - this.data.add("index", (JsonElement)new JsonPrimitive(Integer.valueOf(index))); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/PlayStopComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/PlayStopComposer.java deleted file mode 100644 index bfce7e0d..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/PlayStopComposer.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.audio; - -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class PlayStopComposer extends OutgoingWebMessage { - public PlayStopComposer(boolean isPlaying) { - super("play_stop"); - this.data.add("playing", (JsonElement)new JsonPrimitive(Boolean.valueOf(isPlaying))); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/PlaylistComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/PlaylistComposer.java deleted file mode 100644 index 31692553..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/PlaylistComposer.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.audio; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.audio.RoomPlaylist; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class PlaylistComposer extends OutgoingWebMessage { - public PlaylistComposer(RoomPlaylist playlist) { - super("playlist"); - JsonArray playlistJson = new JsonArray(); - for (RoomPlaylist.YoutubeVideo video : playlist.getPlaylist()) { - JsonObject song = new JsonObject(); - song.add("name", (JsonElement)new JsonPrimitive(video.name)); - song.add("videoId", (JsonElement)new JsonPrimitive(video.videoId)); - song.add("channel", (JsonElement)new JsonPrimitive(video.channel)); - playlistJson.add((JsonElement)song); - } - this.data.add("playlist", (JsonElement)playlistJson); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/RemoveSongComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/RemoveSongComposer.java deleted file mode 100644 index 28db2426..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/audio/RemoveSongComposer.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.audio; - -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class RemoveSongComposer extends OutgoingWebMessage { - public RemoveSongComposer(int index) { - super("remove_song"); - this.data.add("index", (JsonElement)new JsonPrimitive(Integer.valueOf(index))); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/commands/CommandsPopUpComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/commands/CommandsPopUpComposer.java deleted file mode 100644 index 7a5ac1b2..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/commands/CommandsPopUpComposer.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.commands; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.commands.Command; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -public class CommandsPopUpComposer extends OutgoingWebMessage { - public CommandsPopUpComposer(List commands, boolean mod) { - super("commands_pop_up"); - JsonArray json_cmd = new JsonArray(); - Collections.sort(commands, new Comparator() { - public int compare(Command command2, Command command1) { - return Emulator.getTexts().getValue("commands.description." + command2.permission, "commands.description." + command2.permission).compareTo(Emulator.getTexts().getValue("commands.description." + command1.permission, "commands.description." + command1.permission)); - } - }); - for (Command c : commands) - json_cmd.add(Emulator.getTexts().getValue("commands.description." + c.permission, "commands.description." + c.permission)); - this.data.add("commands", (JsonElement)json_cmd); - this.data.add("mod", (JsonElement)new JsonPrimitive(Boolean.valueOf(mod))); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/CommandsComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/CommandsComposer.java deleted file mode 100644 index eb48958b..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/CommandsComposer.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.common; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.commands.Command; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.skeletor.plugin.javascript.categories.Category; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.main; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -public class CommandsComposer extends OutgoingWebMessage { - public CommandsComposer(List commands) { - super("commands"); - JsonArray json_cmd = new JsonArray(); - Collections.sort(commands, new Comparator() { - public int compare(Command command2, Command command1) { - return Emulator.getTexts().getValue("commands.description." + command2.permission, "commands.description." + command2.permission).compareTo(Emulator.getTexts().getValue("commands.description." + command1.permission, "commands.description." + command1.permission)); - } - }); - List duplicateCommands = new ArrayList<>(commands); - List tempList = new ArrayList<>(); - boolean hasPermission = false; - for (Category category : main.getCommandManager().getCommandCategories()) { - tempList = new ArrayList<>(); - hasPermission = false; - if (category.getPermissions().size() > 0) { - for (String permission : category.getPermissions()) { - for (Command command : commands) { - if (command.permission.equals(permission)) { - duplicateCommands.remove(command); - String keys = ""; - if (Emulator.getConfig().getBoolean("categories.cmd_commandsc.show_keys")) { - for (String key : command.keys) { - if (keys.equals("")) { - keys = "(" + key; - } else { - keys = keys + " " + key; - } - } - keys = keys + ")"; - } - tempList.add(Emulator.getTexts().getValue("commands.description." + command.permission, "commands.description." + command.permission + " " + keys)); - hasPermission = true; - } - } - } - if (hasPermission) { - json_cmd.add(category.getName()); - for (String temp : tempList) - json_cmd.add(temp); - } - } - } - if (duplicateCommands.size() > 0 && Emulator.getConfig().getBoolean("categories.cmd_commandsc.show_others")) { - json_cmd.add(Emulator.getTexts().getValue("commands.generic.cmd_commandsc.others")); - for (Command command : duplicateCommands) { - String keys = ""; - if (Emulator.getConfig().getBoolean("categories.cmd_commandsc.show_keys")) { - for (String key : command.keys) { - if (keys.equals("")) { - keys = "(" + key; - } else { - keys = keys + " " + key; - } - } - keys = keys + ")"; - } - json_cmd.add(Emulator.getTexts().getValue("commands.description." + command.permission, "commands.description." + command.permission) + " " + keys); - } - } - this.data.add("commands", (JsonElement)json_cmd); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/OnlineCountComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/OnlineCountComposer.java deleted file mode 100644 index 82528641..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/OnlineCountComposer.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.common; - -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class OnlineCountComposer extends OutgoingWebMessage { - public OnlineCountComposer(int count) { - super("online_count"); - this.data.add("count", (JsonElement)new JsonPrimitive(Integer.valueOf(count))); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/SessionDataComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/SessionDataComposer.java deleted file mode 100644 index 43ed4ec9..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/SessionDataComposer.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.common; - -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class SessionDataComposer extends OutgoingWebMessage { - public SessionDataComposer(int id, String username, String look, int credits) { - super("session_data"); - this.data.add("id", (JsonElement)new JsonPrimitive(Integer.valueOf(id))); - this.data.add("username", (JsonElement)new JsonPrimitive(username)); - this.data.add("look", (JsonElement)new JsonPrimitive(look)); - this.data.add("credits", (JsonElement)new JsonPrimitive(Integer.valueOf(credits))); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/TwitchVideoComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/TwitchVideoComposer.java deleted file mode 100644 index a596c05a..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/TwitchVideoComposer.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.common; - -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class TwitchVideoComposer extends OutgoingWebMessage { - public TwitchVideoComposer(String videoId) { - super("twitch"); - this.data.add("videoId", (JsonElement)new JsonPrimitive(videoId)); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/UpdateCreditsComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/UpdateCreditsComposer.java deleted file mode 100644 index f79bfa3a..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/UpdateCreditsComposer.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.common; - -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class UpdateCreditsComposer extends OutgoingWebMessage { - public UpdateCreditsComposer(int credits) { - super("update_credits"); - this.data.add("credits", (JsonElement)new JsonPrimitive(Integer.valueOf(credits))); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/YoutubeTVComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/YoutubeTVComposer.java deleted file mode 100644 index 69f4e82d..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/common/YoutubeTVComposer.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.common; - -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class YoutubeTVComposer extends OutgoingWebMessage { - public YoutubeTVComposer(String videoId, Integer time) { - super("youtube_tv"); - this.data.add("videoId", (JsonElement)new JsonPrimitive(videoId)); - this.data.add("time", (JsonElement)new JsonPrimitive(time)); - this.data.add("itemId", (JsonElement)new JsonPrimitive(Integer.valueOf(0))); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/slotmachine/SlotMachineComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/slotmachine/SlotMachineComposer.java deleted file mode 100644 index c24da4c3..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/slotmachine/SlotMachineComposer.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.slotmachine; - -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class SlotMachineComposer extends OutgoingWebMessage { - public SlotMachineComposer(int itemId, int credits) { - super("slot_machine"); - this.data.add("itemId", (JsonElement)new JsonPrimitive(Integer.valueOf(itemId))); - this.data.add("credits", (JsonElement)new JsonPrimitive(Integer.valueOf(credits))); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/slotmachine/SpinResultComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/slotmachine/SpinResultComposer.java deleted file mode 100644 index 54cbc9a4..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/communication/outgoing/slotmachine/SpinResultComposer.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.skeletor.plugin.javascript.communication.outgoing.slotmachine; - -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; - -public class SpinResultComposer extends OutgoingWebMessage { - public SpinResultComposer(int result1, int result2, int result3, boolean won, int payout) { - super("slot_result"); - this.data.add("result1", (JsonElement)new JsonPrimitive(Integer.valueOf(result1))); - this.data.add("result2", (JsonElement)new JsonPrimitive(Integer.valueOf(result2))); - this.data.add("result3", (JsonElement)new JsonPrimitive(Integer.valueOf(result3))); - this.data.add("won", (JsonElement)new JsonPrimitive(Boolean.valueOf(won))); - this.data.add("payout", (JsonElement)new JsonPrimitive(Integer.valueOf(payout))); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/interactions/InteractionSlotMachine.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/interactions/InteractionSlotMachine.java deleted file mode 100644 index 37321b56..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/interactions/InteractionSlotMachine.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.skeletor.plugin.javascript.interactions; - -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.habbohotel.items.Item; -import com.eu.habbo.habbohotel.items.interactions.InteractionDefault; -import com.eu.habbo.habbohotel.rooms.Room; -import com.eu.habbo.habbohotel.rooms.RoomUnit; -import com.eu.habbo.messages.outgoing.MessageComposer; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.slotmachine.SlotMachineComposer; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; -import java.sql.ResultSet; -import java.sql.SQLException; - -public class InteractionSlotMachine extends InteractionDefault { - public InteractionSlotMachine(ResultSet set, Item baseItem) throws SQLException { - super(set, baseItem); - } - - public InteractionSlotMachine(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { - super(id, userId, item, extradata, limitedStack, limitedSells); - } - - public boolean canWalkOn(RoomUnit roomUnit, Room room, Object[] objects) { - return false; - } - - public boolean isWalkable() { - return false; - } - - public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception {} - - public void onClick(GameClient client, Room room, Object[] objects) throws Exception { - super.onClick(client, room, objects); - SlotMachineComposer webComposer = new SlotMachineComposer(getId(), client.getHabbo().getHabboInfo().getCredits()); - client.sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)webComposer)); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/interactions/InteractionYoutubeJukebox.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/interactions/InteractionYoutubeJukebox.java deleted file mode 100644 index c28706b1..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/interactions/InteractionYoutubeJukebox.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.skeletor.plugin.javascript.interactions; - -import com.eu.habbo.habbohotel.gameclients.GameClient; -import com.eu.habbo.habbohotel.items.Item; -import com.eu.habbo.habbohotel.items.interactions.InteractionDefault; -import com.eu.habbo.habbohotel.rooms.Room; -import com.eu.habbo.habbohotel.rooms.RoomUnit; -import com.eu.habbo.messages.outgoing.MessageComposer; -import com.skeletor.plugin.javascript.audio.RoomAudioManager; -import com.skeletor.plugin.javascript.audio.RoomPlaylist; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.audio.JukeboxComposer; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; -import java.sql.ResultSet; -import java.sql.SQLException; - -public class InteractionYoutubeJukebox extends InteractionDefault { - public InteractionYoutubeJukebox(ResultSet set, Item baseItem) throws SQLException { - super(set, baseItem); - } - - public InteractionYoutubeJukebox(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) { - super(id, userId, item, extradata, limitedStack, limitedSells); - } - - public boolean canWalkOn(RoomUnit roomUnit, Room room, Object[] objects) { - return false; - } - - public boolean isWalkable() { - return false; - } - - public void onWalk(RoomUnit roomUnit, Room room, Object[] objects) throws Exception {} - - public void onClick(GameClient client, Room room, Object[] objects) throws Exception { - super.onClick(client, room, objects); - if (room.hasRights(client.getHabbo())) { - RoomPlaylist roomPlaylist = RoomAudioManager.getInstance().getPlaylistForRoom(room.getId()); - JukeboxComposer webComposer = new JukeboxComposer(roomPlaylist); - client.sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)webComposer)); - } - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/main.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/main.java deleted file mode 100644 index 53a101ab..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/main.java +++ /dev/null @@ -1,197 +0,0 @@ -package com.skeletor.plugin.javascript; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.commands.Command; -import com.eu.habbo.habbohotel.commands.CommandHandler; -import com.eu.habbo.habbohotel.items.ItemInteraction; -import com.eu.habbo.habbohotel.users.Habbo; -import com.eu.habbo.messages.outgoing.MessageComposer; -import com.eu.habbo.plugin.EventHandler; -import com.eu.habbo.plugin.EventListener; -import com.eu.habbo.plugin.HabboPlugin; -import com.eu.habbo.plugin.events.emulator.EmulatorLoadItemsManagerEvent; -import com.eu.habbo.plugin.events.emulator.EmulatorLoadedEvent; -import com.eu.habbo.plugin.events.rooms.RoomUncachedEvent; -import com.eu.habbo.plugin.events.users.UserCreditsEvent; -import com.eu.habbo.plugin.events.users.UserEnterRoomEvent; -import com.eu.habbo.plugin.events.users.UserExitRoomEvent; -import com.eu.habbo.plugin.events.users.UserLoginEvent; -import com.eu.habbo.plugin.events.users.UserSavedSettingsEvent; -import com.skeletor.plugin.javascript.audio.RoomAudioManager; -import com.skeletor.plugin.javascript.audio.RoomPlaylist; -import com.skeletor.plugin.javascript.categories.CommandManager; -import com.skeletor.plugin.javascript.commands.CmdCommand; -import com.skeletor.plugin.javascript.commands.TwitchCommand; -import com.skeletor.plugin.javascript.commands.UpdateCategoriesCommand; -import com.skeletor.plugin.javascript.commands.YoutubeCommand; -import com.skeletor.plugin.javascript.communication.CommunicationManager; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.audio.ChangeVolumeComposer; -import com.skeletor.plugin.javascript.communication.outgoing.audio.DisposePlaylistComposer; -import com.skeletor.plugin.javascript.communication.outgoing.audio.PlaySongComposer; -import com.skeletor.plugin.javascript.communication.outgoing.audio.PlayStopComposer; -import com.skeletor.plugin.javascript.communication.outgoing.audio.PlaylistComposer; -import com.skeletor.plugin.javascript.communication.outgoing.common.OnlineCountComposer; -import com.skeletor.plugin.javascript.communication.outgoing.common.SessionDataComposer; -import com.skeletor.plugin.javascript.communication.outgoing.common.TwitchVideoComposer; -import com.skeletor.plugin.javascript.communication.outgoing.common.UpdateCreditsComposer; -import com.skeletor.plugin.javascript.interactions.InteractionSlotMachine; -import com.skeletor.plugin.javascript.interactions.InteractionYoutubeJukebox; -import com.skeletor.plugin.javascript.override_packets.incoming.JavascriptCallbackEvent; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; -import com.skeletor.plugin.javascript.runnables.OnlineCountRunnable; -import com.skeletor.plugin.javascript.scheduler.CheckLoadedScheduler; -import gnu.trove.map.hash.THashMap; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; - -public class main extends HabboPlugin implements EventListener { - public static final int JSCALLBACKEVENTHEADER = 314; - - private static CommandManager commandManager; - - private static THashMap twitchRooms; - - public static String USER_LOADED_EVENT = "user.loaded.event.key"; - - public static String STARTED_LOADING_EVENT = "user.loading.event.key"; - - public void onEnable() throws Exception { - Emulator.getPluginManager().registerEvents(this, this); - if (Emulator.isReady && !Emulator.isShuttingDown) - onEmulatorLoadedEvent(null); - } - - @EventHandler - public void onEmulatorLoadedEvent(EmulatorLoadedEvent e) throws Exception { - Emulator.getGameServer().getPacketManager().registerHandler(Integer.valueOf(314), JavascriptCallbackEvent.class); - Emulator.getTexts().register("jscommands.keys.cmd_commands", "commands"); - Emulator.getTexts().register("commands.description.cmd_commandsc", ":commandsc"); - Emulator.getTexts().register("categories.cmd_commandsc.keys", "commandsc"); - Emulator.getTexts().register("commands.generic.cmd_commandsc.text", "Your Commands"); - Emulator.getTexts().register("commands.generic.cmd_commandsc.others", "Others"); - Emulator.getTexts().register("commands.description.cmd_update_categories", ":update_categories"); - Emulator.getTexts().register("categories.cmd_update_categories.keys", "update_categories"); - Emulator.getTexts().register("categories.cmd_update_categories.success", "Successfully updated command categories"); - Emulator.getConfig().register("categories.cmd_commandsc.show_keys", "1"); - Emulator.getConfig().register("categories.cmd_commandsc.show_others", "1"); - boolean reloadPermissions = false; - reloadPermissions = registerPermission("cmd_commandsc", "'0', '1'", "1", reloadPermissions); - reloadPermissions = registerPermission("cmd_update_categories", "'0', '1'", "0", reloadPermissions); - if (reloadPermissions) - Emulator.getGameEnvironment().getPermissionsManager().reload(); - CommandHandler.addCommand((Command)new YoutubeCommand()); - CommandHandler.addCommand((Command)new CmdCommand()); - CommandHandler.addCommand((Command)new TwitchCommand()); - CommandHandler.addCommand((Command)new UpdateCategoriesCommand("cmd_update_categories", Emulator.getTexts().getValue("categories.cmd_update_categories.keys").split(";"))); - RoomAudioManager.Init(); - OnlineCountRunnable.getInstance().start(); - commandManager = new CommandManager(); - twitchRooms = new THashMap(); - new CheckLoadedScheduler(); - } - - private boolean registerPermission(String name, String options, String defaultValue, boolean defaultReturn) { - try(Connection connection = Emulator.getDatabase().getDataSource().getConnection(); - - PreparedStatement statement = connection.prepareStatement("ALTER TABLE `permissions` ADD `" + name + "` ENUM( " + options + " ) NOT NULL DEFAULT '" + defaultValue + "'")) { - statement.execute(); - return true; - } catch (SQLException sQLException) { - return defaultReturn; - } - } - - @EventHandler - public void onLoadItemsManager(EmulatorLoadItemsManagerEvent e) { - Emulator.getGameEnvironment().getItemManager().addItemInteraction(new ItemInteraction("slots_machine", InteractionSlotMachine.class)); - Emulator.getGameEnvironment().getItemManager().addItemInteraction(new ItemInteraction("yt_jukebox", InteractionYoutubeJukebox.class)); - } - - @EventHandler - public void onUserEnterRoomEvent(UserEnterRoomEvent e) { - RoomPlaylist playlist = RoomAudioManager.getInstance().getPlaylistForRoom(e.room.getId()); - if (playlist.getPlaylist().size() > 0) { - e.habbo.getClient().sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)new PlaylistComposer(playlist))); - if (playlist.isPlaying()) { - e.habbo.getClient().sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)new PlaySongComposer(playlist.getCurrentIndex()))); - e.habbo.getClient().sendResponse(playlist.getNowPlayingBubbleAlert()); - } - } - if (e.room.getHabbos() == null || e.room.getHabbos().size() == 0) - twitchRooms.remove(Integer.valueOf(e.room.getId())); - if (twitchRooms.containsKey(Integer.valueOf(e.room.getId()))) { - TwitchVideoComposer twitchVideoComposer = new TwitchVideoComposer((String)twitchRooms.get(Integer.valueOf(e.room.getId()))); - e.habbo.getClient().sendResponse((new JavascriptCallbackComposer((OutgoingWebMessage)twitchVideoComposer)).compose()); - } - } - - @EventHandler - public void onUserExitRoomEvent(UserExitRoomEvent e) { - e.habbo.getClient().sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)new PlayStopComposer(false))); - e.habbo.getClient().sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)new DisposePlaylistComposer())); - } - - @EventHandler - public void onRoomUncachedEvent(RoomUncachedEvent e) { - RoomAudioManager.getInstance().dispose(e.room.getId()); - } - - @EventHandler - public void onUserLoginEvent(UserLoginEvent e) { - if (e.habbo == null || e.habbo.getClient() == null) - return; - Habbo habbo = e.habbo; - (e.habbo.getHabboStats()).cache.put(USER_LOADED_EVENT, Boolean.valueOf(false)); - (e.habbo.getHabboStats()).cache.put(STARTED_LOADING_EVENT, Integer.valueOf(Emulator.getIntUnixTimestamp())); - SessionDataComposer sessionDataComposer = new SessionDataComposer(e.habbo.getHabboInfo().getId(), e.habbo.getHabboInfo().getUsername(), e.habbo.getHabboInfo().getLook(), e.habbo.getHabboInfo().getCredits()); - e.habbo.getClient().sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)sessionDataComposer)); - e.habbo.getClient().sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)new ChangeVolumeComposer((e.habbo.getHabboStats()).volumeTrax))); - e.habbo.getClient().sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)new OnlineCountComposer(Emulator.getGameEnvironment().getHabboManager().getOnlineCount()))); - Emulator.getThreading().run(() -> userCheckLoaded(habbo), 500L); - } - - public void userCheckLoaded(Habbo habbo) { - if (habbo == null || habbo.getClient() == null || !habbo.isOnline()) - return; - if (((Boolean)(habbo.getHabboStats()).cache.get(USER_LOADED_EVENT)).booleanValue()) - return; - if (Emulator.getIntUnixTimestamp() - ((Integer)(habbo.getHabboStats()).cache.get(STARTED_LOADING_EVENT)).intValue() > 15) - return; - SessionDataComposer sessionDataComposer = new SessionDataComposer(habbo.getHabboInfo().getId(), habbo.getHabboInfo().getUsername(), habbo.getHabboInfo().getLook(), habbo.getHabboInfo().getCredits()); - habbo.getClient().sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)sessionDataComposer)); - habbo.getClient().sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)new ChangeVolumeComposer((habbo.getHabboStats()).volumeTrax))); - habbo.getClient().sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)new OnlineCountComposer(Emulator.getGameEnvironment().getHabboManager().getOnlineCount()))); - Emulator.getThreading().run(() -> userCheckLoaded(habbo), 500L); - } - - @EventHandler - public void onUserCreditsEvent(UserCreditsEvent e) { - UpdateCreditsComposer creditsComposer = new UpdateCreditsComposer(e.habbo.getHabboInfo().getCredits()); - e.habbo.getClient().sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)creditsComposer)); - } - - @EventHandler - public void onUserSavedSettingsEvent(UserSavedSettingsEvent e) { - e.habbo.getClient().sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)new ChangeVolumeComposer((e.habbo.getHabboStats()).volumeTrax))); - } - - public void onDisable() throws Exception { - CommunicationManager.getInstance().Dispose(); - RoomAudioManager.getInstance().Dispose(); - OnlineCountRunnable.getInstance().stop(); - } - - public static CommandManager getCommandManager() { - return commandManager; - } - - public static void addTwitchRoom(int roomId, String twitch) { - twitchRooms.put(Integer.valueOf(roomId), twitch); - } - - public boolean hasPermission(Habbo habbo, String s) { - return false; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/override_packets/incoming/JavascriptCallbackEvent.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/override_packets/incoming/JavascriptCallbackEvent.java deleted file mode 100644 index ddda6429..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/override_packets/incoming/JavascriptCallbackEvent.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.skeletor.plugin.javascript.override_packets.incoming; - -import com.eu.habbo.messages.incoming.MessageHandler; -import com.skeletor.plugin.javascript.communication.CommunicationManager; - -public class JavascriptCallbackEvent extends MessageHandler { - public void handle() { - String payload = this.packet.readString(); - CommunicationManager.getInstance().OnMessage(payload, this.client); - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/override_packets/outgoing/JavascriptCallbackComposer.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/override_packets/outgoing/JavascriptCallbackComposer.java deleted file mode 100644 index 61178ab6..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/override_packets/outgoing/JavascriptCallbackComposer.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.skeletor.plugin.javascript.override_packets.outgoing; - -import com.eu.habbo.messages.ServerMessage; -import com.eu.habbo.messages.outgoing.MessageComposer; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.utils.JsonFactory; - -public class JavascriptCallbackComposer extends MessageComposer { - private OutgoingWebMessage webMessage; - - public JavascriptCallbackComposer(OutgoingWebMessage message) { - this.webMessage = message; - } - - public ServerMessage composeInternal() { - this.response.init(2023); - String jsonMessage = JsonFactory.getInstance().toJson(this.webMessage).replace("/", "/"); - this.response.appendString("habblet/open/" + jsonMessage); - return this.response; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/runnables/OnlineCountRunnable.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/runnables/OnlineCountRunnable.java deleted file mode 100644 index 54558399..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/runnables/OnlineCountRunnable.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.skeletor.plugin.javascript.runnables; - -import com.eu.habbo.Emulator; -import com.eu.habbo.messages.outgoing.MessageComposer; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.common.OnlineCountComposer; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; - -public class OnlineCountRunnable implements Runnable { - private static final OnlineCountRunnable instance = new OnlineCountRunnable(); - - private volatile boolean running = false; - - public void run() { - if (this.running) { - int count = Emulator.getGameEnvironment().getHabboManager().getOnlineCount(); - Emulator.getGameServer().getGameClientManager().sendBroadcastResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)new OnlineCountComposer(count))); - Emulator.getThreading().run(this, 30000L); - } - } - - public static OnlineCountRunnable getInstance() { - return instance; - } - - public void start() { - if (!this.running) { - this.running = true; - Emulator.getThreading().run(this); - } - } - - public void stop() { - this.running = false; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/scheduler/CheckLoadedScheduler.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/scheduler/CheckLoadedScheduler.java deleted file mode 100644 index 030bca3d..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/scheduler/CheckLoadedScheduler.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.skeletor.plugin.javascript.scheduler; - -import com.eu.habbo.Emulator; -import com.eu.habbo.core.Scheduler; -import com.eu.habbo.habbohotel.users.Habbo; -import com.eu.habbo.messages.outgoing.MessageComposer; -import com.skeletor.plugin.javascript.communication.outgoing.OutgoingWebMessage; -import com.skeletor.plugin.javascript.communication.outgoing.common.SessionDataComposer; -import com.skeletor.plugin.javascript.main; -import com.skeletor.plugin.javascript.override_packets.outgoing.JavascriptCallbackComposer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CheckLoadedScheduler extends Scheduler { - private static final Logger LOGGER = LoggerFactory.getLogger(CheckLoadedScheduler.class); - - public CheckLoadedScheduler() { - super(60); - run(); - } - - public void run() { - super.run(); - if (Emulator.getConfig().getBoolean("pop.up.enabled", true)) - for (Habbo habbo : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().values()) { - if (!(habbo.getHabboStats()).cache.containsKey(main.USER_LOADED_EVENT)) - continue; - if (((Boolean)(habbo.getHabboStats()).cache.get(main.USER_LOADED_EVENT)).booleanValue()) - continue; - SessionDataComposer sessionDataComposer = new SessionDataComposer(habbo.getHabboInfo().getId(), habbo.getHabboInfo().getUsername(), habbo.getHabboInfo().getLook(), habbo.getHabboInfo().getCredits()); - habbo.getClient().sendResponse((MessageComposer)new JavascriptCallbackComposer((OutgoingWebMessage)sessionDataComposer)); - } - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/utils/JsonFactory.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/utils/JsonFactory.java deleted file mode 100644 index fa3e5abd..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/utils/JsonFactory.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.skeletor.plugin.javascript.utils; - -import com.google.gson.Gson; - -public class JsonFactory { - private static final Gson gson = new Gson(); - - public static Gson getInstance() { - return gson; - } -} diff --git a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/utils/RegexUtility.java b/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/utils/RegexUtility.java deleted file mode 100644 index 9fb748d6..00000000 --- a/Plugins/Javascript-Plugin/src/main/java/com/skeletor/plugin/javascript/utils/RegexUtility.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.skeletor.plugin.javascript.utils; -import org.owasp.html.HtmlPolicyBuilder; -import org.owasp.html.PolicyFactory; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class RegexUtility { - - public static String getYouTubeId (String youTubeUrl) { - String pattern = "(?<=youtu.be/|watch\\?v=|/videos/|embed\\/)[^#\\&\\?]*"; - Pattern compiledPattern = Pattern.compile(pattern); - Matcher matcher = compiledPattern.matcher(youTubeUrl); - if(matcher.find()){ - return matcher.group(); - } else { - return ""; - } - } - - /** - * Sanitizes a string by removing any potentially harmful HTML elements. - * - * @param str The string to be sanitized. - * @return The sanitized string. - */ - public static String sanitize(String str) { - PolicyFactory policy = new HtmlPolicyBuilder().toFactory(); - return policy.sanitize(str); - } -} \ No newline at end of file diff --git a/Plugins/Javascript-Plugin/src/main/resources/plugin.json b/Plugins/Javascript-Plugin/src/main/resources/plugin.json deleted file mode 100644 index 64adabf1..00000000 --- a/Plugins/Javascript-Plugin/src/main/resources/plugin.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "main" : "com.skeletor.plugin.javascript.main", - "name" : "Javascript Plugin", - "author" : "skeletor" -} \ No newline at end of file diff --git a/docs/builders_club_catalog_reference.md b/docs/builders_club_catalog_reference.md new file mode 100644 index 00000000..d77961ae --- /dev/null +++ b/docs/builders_club_catalog_reference.md @@ -0,0 +1,284 @@ +# Builders Club Catalog Reference + +Questa guida riassume il setup corretto dopo la separazione completa tra catalogo normale e `Builders Club`. + +## Tabelle usate davvero + +- Catalogo normale: + - `catalog_pages` + - `catalog_items` +- Builders Club: + - `catalog_pages_bc` + - `catalog_items_bc` +- Abbonamenti / add-on BC venduti nel catalogo normale: + - `catalog_club_offers` + +Quindi: + +- se vuoi una pagina BC, va in `catalog_pages_bc` +- se vuoi un furni BC, va in `catalog_items_bc` +- se vuoi vendere lo stesso furni anche nel catalogo normale, aggiungi un'altra riga normale in `catalog_items` + +Questo è proprio il vantaggio della separazione: lo stesso `item_id` può comparire sia nel catalogo normale sia nel BC, ma con comportamenti diversi. + +## Differenza pratica tra catalogo normale e BC + +### Catalogo normale + +- gli offer arrivano da `catalog_items` +- hanno costi normali (`cost_credits`, `cost_points`, ecc.) +- quando comprati diventano proprietà utente / inventario + +### Builders Club + +- gli offer arrivano da `catalog_items_bc` +- non hanno prezzo perché il piazzamento BC usa il flow dedicato +- non entrano nell'inventario utente +- non diventano mai proprietà utente +- quando rimossi dalla stanza vengono eliminati + +## Migration da applicare + +Assicurati di avere applicato: + +- `Database Updates/009_add_builders_club_catalog_offers.sql` +- `Database Updates/010_add_catalog_mode_to_catalog_pages.sql` +- `Database Updates/011_add_builders_club_trial_room_lock.sql` +- `Database Updates/012_support_builders_club_catalog_tables.sql` + +La `012` è importante perché aggiorna `catalog_pages_bc.page_layout` con i layout BC moderni: + +- `builders_club_frontpage` +- `builders_club_addons` +- `builders_club_loyalty` + +## Come aggiungere pagine BC + +Le pagine BC vanno create in `catalog_pages_bc`. + +Esempio: + +```sql +INSERT INTO catalog_pages_bc +( + parent_id, + caption, + page_layout, + icon_color, + icon_image, + order_num, + visible, + enabled, + page_headline, + page_teaser, + page_special, + page_text1, + page_text2, + page_text_details, + page_text_teaser +) +VALUES +( + -1, + 'Builders Furni', + 'default_3x3', + 1, + 28, + 1, + '1', + '1', + 'catalog_header_roombuilder', + '', + '', + 'Builders Club', + 'Linea test', + 'Pagina test del Builders Club', + '' +); +``` + +## Come aggiungere furni BC + +I furni BC vanno in `catalog_items_bc`. + +Esempio: + +```sql +INSERT INTO catalog_items_bc +( + item_ids, + page_id, + catalog_name, + order_number, + extradata +) +VALUES +( + '12345', + 1, + 'bc_test_sofa', + 1, + '' +); +``` + +Dove: + +- `item_ids` = ID del base item +- `page_id` = ID pagina in `catalog_pages_bc` +- `catalog_name` = chiave offer/localization + +## Come vendere lo stesso furni anche nel catalogo normale + +Se vuoi che lo stesso furni sia: + +- vendibile nel catalogo normale +- disponibile anche nel Builders Club + +devi avere **due righe distinte**: + +### Normale + +```sql +INSERT INTO catalog_items +( + page_id, + item_ids, + catalog_name, + cost_credits, + cost_points, + points_type, + amount, + club_only, + extradata, + have_offer, + offer_id, + limited_stack, + order_number +) +VALUES +( + 500, + '12345', + 'normal_test_sofa', + 5, + 0, + 0, + 1, + '0', + '', + '1', + -1, + 0, + 1 +); +``` + +### Builders Club + +```sql +INSERT INTO catalog_items_bc +( + item_ids, + page_id, + catalog_name, + order_number, + extradata +) +VALUES +( + '12345', + 1, + 'bc_test_sofa', + 1, + '' +); +``` + +Quindi lo stesso base item `12345` può vivere in entrambi i cataloghi senza condividere il prezzo. + +## Abbonamento e add-on BC + +Abbonamento e add-on non stanno in `catalog_items_bc`. + +Vanno in: + +- `catalog_club_offers` + +Tipi supportati: + +- `BUILDERS_CLUB` +- `BUILDERS_CLUB_ADDON` + +Sono venduti nel catalogo normale, come HC/VIP, ma il widget BC usa comunque le sue pagine dedicate da `catalog_pages_bc`. + +## Nota su `catalog_mode` + +`catalog_mode` resta nella tabella `catalog_pages`, ma non è più il meccanismo principale per far comparire le pagine nel Builders Club. + +Adesso il runtime BC legge direttamente: + +- `catalog_pages_bc` +- `catalog_items_bc` + +Quindi: + +- aggiungere pagine BC in `catalog_pages` non basta +- aggiungere items BC in `catalog_items` non basta +- usare `BOTH` su una pagina normale non la renderà automaticamente una pagina BC + +## Query utili per test + +### Elencare pagine BC + +```sql +SELECT * FROM catalog_pages_bc ORDER BY parent_id, order_num, id; +``` + +### Elencare items BC + +```sql +SELECT * FROM catalog_items_bc ORDER BY page_id, order_number, id; +``` + +### Trovare lo stesso furni in entrambi i cataloghi + +```sql +SELECT 'NORMAL' AS source, id, page_id, item_ids, catalog_name +FROM catalog_items +WHERE item_ids = '12345' + +UNION ALL + +SELECT 'BC' AS source, id, page_id, item_ids, catalog_name +FROM catalog_items_bc +WHERE item_ids = '12345'; +``` + +## Consiglio pratico + +Per fare test rapidi: + +1. crea una pagina in `catalog_pages_bc` +2. inserisci 1-2 furni in `catalog_items_bc` +3. lascia gli stessi furni anche in `catalog_items` se li vuoi vendibili normalmente +4. pubblica / ricarica il catalogo + +Se vuoi, possiamo aggiungere anche un file SQL separato con qualche pagina BC e qualche furni BC già pronti da importare per i test. + +## Seed demo già pronto + +Se vuoi una demo immediata, puoi usare: + +- `Database Updates/013_seed_builders_club_sample_page.sql` + +Questo seed: + +- crea una root BC demo +- crea una pagina BC demo figlia +- duplica alcuni furni già esistenti del catalogo normale dentro `catalog_items_bc` + +Così puoi testare subito il caso: + +- stesso furni vendibile nel catalogo normale +- stesso furni disponibile anche nel Builders Club diff --git a/docs/emulator_settings_reference.md b/docs/emulator_settings_reference.md new file mode 100644 index 00000000..a8911c7e --- /dev/null +++ b/docs/emulator_settings_reference.md @@ -0,0 +1,781 @@ +# Emulator Settings Reference + +## Scope + +This document inventories the non-wired keys currently stored in `emulator_settings` based on `Default Database/FullDB.sql`. Wired-specific keys are documented separately in `docs/wired_tools_reference.md`. + +Each entry below mirrors the comment written by `Database Updates/003_add_comment_column_to_emulator_settings.sql`, so the documentation and in-database comments stay aligned. + +## Table schema + +```sql +CREATE TABLE `emulator_settings` ( + `key` varchar(100) NOT NULL, + `value` varchar(512) NOT NULL, + `comment` text NOT NULL, + PRIMARY KEY (`key`) +); +``` + +## Inventory summary + +- Total non-wired keys documented here: `329` +- Source of defaults: `Default Database/FullDB.sql` +- Value type is inferred from the default string stored in SQL. + +## Group index + +- `allowed` (1) +- `apollyon` (1) +- `basejump` (2) +- `bots` (1) +- `bubblealerts` (6) +- `bundle` (2) +- `callback` (3) +- `camera` (11) +- `catalog` (5) +- `commands` (3) +- `console` (1) +- `custom` (1) +- `db` (4) +- `debug` (7) +- `discount` (5) +- `easter_eggs` (1) +- `enc` (4) +- `essentials` (2) +- `flood` (1) +- `ftp` (4) +- `furniture` (1) +- `gamecenter` (16) +- `gamedata` (1) +- `guardians` (5) +- `hotel` (169) +- `hotelview` (5) +- `imager` (6) +- `images` (2) +- `info` (1) +- `invisible` (1) +- `io` (3) +- `logging` (6) +- `marketplace` (1) +- `monsterplant` (2) +- `moodlight` (1) +- `navigator` (1) +- `networking` (1) +- `notify` (1) +- `path` (1) +- `pathfinder` (4) +- `pirate_parrot` (2) +- `postit` (1) +- `pyramids` (1) +- `retro` (1) +- `room` (4) +- `rosie` (2) +- `runtime` (1) +- `save` (2) +- `scripter` (1) +- `seasonal` (7) +- `subscriptions` (12) +- `team` (1) +- `youtube` (1) + +## `allowed` + +Validation rules for usernames and account-facing inputs. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `allowed.username.characters` | `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=!?@:,.` | `list` | Characters allowed when users choose or change a username. | + +## `apollyon` + +Custom project-specific behaviour switches. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `apollyon.cooldown.amount` | `250` | `integer` | Cooldown in milliseconds used by the Apollyon-specific behaviour or command flow. | + +## `basejump` + +BaseJump or FastFood launcher URLs. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `basejump.assets.url` | `http://localhost/gamecenter/gamecenter_basejump/BasicAssets.swf` | `url` | Asset URL used by the BaseJump or FastFood game client. | +| `basejump.url` | `http://localhost/game/BaseJump.swf` | `url` | SWF URL used to launch the BaseJump or FastFood game client. | + +## `bots` + +Miscellaneous visitor-bot display settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `bots.visitor.dateformat` | `yyyy-mm-dd HH:mm` | `string` | Date format used by visitor bots when they print timestamps. | + +## `bubblealerts` + +Bubble notification behaviour and assets. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `bubblealerts.enabled` | `1` | `boolean` | Master switch for bubble alert notifications. | +| `bubblealerts.notif_friendonline.enabled` | `1` | `boolean` | Enable bubble alerts when friends come online. | +| `bubblealerts.notif_friendonline.image` | `${image.library.url}notifications/figure?p=%figure%` | `template` | Image template used when showing friend-online bubble alerts. | +| `bubblealerts.notif_friendonline.useimage` | `1` | `boolean` | Use the configured figure image inside friend-online bubble alerts. | +| `bubblealerts.notif_marketplace.enabled` | `1` | `boolean` | Show bubble alerts for marketplace notifications. | +| `bubblealerts.notif_purchase.limited` | `0` | `boolean` | Show bubble alerts for limited-item purchases. | + +## `bundle` + +Bundle-specific toggles for pets and bots. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `bundle.bots.enabled` | `1` | `boolean` | Allow bots to be included in room bundles or package rewards. | +| `bundle.pets.enabled` | `1` | `boolean` | Allow pets to be included in room bundles or package rewards. | + +## `callback` + +HTTP callback integrations for external services. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `callback.get.version` | `1` | `boolean` | Enable the GET callback used to report version to external services. | +| `callback.post.errors` | `1` | `boolean` | Enable the POST callback used to report errors to external services. | +| `callback.post.statistics` | `1` | `boolean` | Enable the POST callback used to report statistics to external services. | + +## `camera` + +Camera costs, storage and publish settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `camera.enabled` | `1` | `boolean` | Enable the in-room camera feature. | +| `camera.extradata` | `{\"t\":%timestamp%, \"u\":\"%id%\", \"s\":%room_id%, \"w\":\"%url%\"}` | `template` | Extradata template written into camera photo items when they are created. | +| `camera.item_id` | `45970` | `integer` | Base item ID used by the generated camera photo furniture. | +| `camera.price.credits` | `2` | `integer` | Credit price charged when taking a camera photo. | +| `camera.price.points` | `0` | `boolean` | Amount of activity points charged when taking a camera photo. | +| `camera.price.points.publish` | `10` | `integer` | Amount of activity points charged when publishing a camera photo. | +| `camera.price.points.publish.type` | `0` | `boolean` | Activity point type used for the camera publish cost. | +| `camera.price.points.type` | `0` | `boolean` | Activity point type used for the camera capture cost. | +| `camera.publish.delay` | `180` | `integer` | Delay in seconds before a published camera photo becomes available. | +| `camera.url` | `http://localhost/usercontent/camera/` | `url` | Base URL where camera images are published. | +| `camera.use.https` | `1` | `boolean` | Force HTTPS when generating camera image URLs. | + +## `catalog` + +Catalog behaviour that is not wired-specific. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `catalog.guild.hc_required` | `1` | `boolean` | Require HC or VIP status before users can create a guild. | +| `catalog.guild.price` | `10` | `integer` | Credit cost required to create a guild. | +| `catalog.ltd.page.soldout` | `761` | `integer` | Layout or image ID used when a limited page is sold out. | +| `catalog.ltd.random` | `1` | `boolean` | Randomize the order or selection of limited catalog items. | +| `catalog.page.vipgifts` | `0` | `boolean` | Catalog page ID used for VIP gift redemption. | + +## `commands` + +Command-specific restrictions and compatibility flags. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `commands.cmd_chatcolor.banned_numbers` | `23;33;34` | `list` | Semicolon-separated list of chat color IDs blocked for the chatcolor command. | +| `commands.cmd_staffonline.min_rank` | `2` | `integer` | Minimum permission rank required to use the staffonline command. | +| `commands.plugins.oldstyle` | `0` | `boolean` | Use the legacy command plugin loading style. | + +## `console` + +Console behaviour. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `console.mode` | `1` | `boolean` | Controls the emulator console mode or console output style. | + +## `custom` + +Fork-specific custom behaviour switches. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `custom.stacking.enabled` | `0` | `boolean` | Enable custom item stacking behaviour outside the default stacking rules. | + +## `db` + +Database pooling and batching controls. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `db.max.partition.size` | `2` | `integer` | Maximum batch or partition size used by partitioned database operations. | +| `db.min.partition.size` | `1` | `boolean` | Minimum batch or partition size used by partitioned database operations. | +| `db.pool.maxsize` | `12` | `integer` | Maximum size of the database connection pool. | +| `db.pool.minsize` | `8` | `integer` | Minimum number of open connections kept in the database pool. | + +## `debug` + +Verbose debug output toggles. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `debug.mode` | `1` | `boolean` | Enable general emulator debug mode. | +| `debug.show.errors` | `1` | `boolean` | Show internal debug error messages. | +| `debug.show.headers` | `0` | `boolean` | Show packet headers in debug logs. | +| `debug.show.packets` | `0` | `boolean` | Print packet-level debug output. | +| `debug.show.packets.undefined` | `0` | `boolean` | Print debug output for undefined incoming or outgoing packets. | +| `debug.show.sql.exception` | `1` | `boolean` | Log SQL exceptions to the console. | +| `debug.show.users` | `1` | `boolean` | Show user-related debug messages. | + +## `discount` + +Discount batch rules for catalog purchases. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `discount.additional.thresholds` | `40;99` | `list` | Semicolon-separated discount thresholds used for extra batch bonuses. | +| `discount.batch.free.items` | `1` | `boolean` | Number of free items granted inside one discount batch. | +| `discount.batch.size` | `6` | `integer` | Number of items required for one discount batch. | +| `discount.bonus.min.discounts` | `1` | `boolean` | Minimum number of discount batches required before the bonus logic applies. | +| `discount.max.allowed.items` | `100` | `integer` | Maximum number of catalog items that can participate in one discount batch. | + +## `easter_eggs` + +Optional easter egg features. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `easter_eggs.enabled` | `1` | `boolean` | Enable or disable the feature controlled by `easter_eggs.enabled`. | + +## `enc` + +Encryption and RSA settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `enc.d` | `` | `string` | RSA private exponent used by the encryption layer. | +| `enc.e` | `` | `string` | RSA public exponent used by the encryption layer. | +| `enc.enabled` | `1` | `boolean` | Enable RSA encryption support for the socket handshake. | +| `enc.n` | `` | `string` | RSA modulus used by the encryption layer. | + +## `essentials` + +Essentials plugin or command values. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `essentials.cmd_kill.effect.killer` | `164;182` | `list` | Semicolon-separated effect IDs used by the kill command for the killer. | +| `essentials.cmd_kill.effect.victim` | `93;89` | `list` | Semicolon-separated effect IDs used by the kill command for the victim. | + +## `flood` + +Flood-control compatibility switches. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `flood.with.rights` | `0` | `boolean` | Allow users with room rights to bypass the normal flood protection. | + +## `ftp` + +FTP integration settings for generated assets. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `ftp.enabled` | `0` | `boolean` | Enable FTP uploads for generated assets. | +| `ftp.host` | `example.com` | `string` | FTP host used for asset uploads. | +| `ftp.password` | `password123` | `string` | FTP password used for asset uploads. | +| `ftp.user` | `root` | `string` | FTP username used for asset uploads. | + +## `furniture` + +General furniture interaction behaviour. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `furniture.talking.range` | `2` | `integer` | Maximum tile distance at which talking furniture can react to nearby speech. | + +## `gamecenter` + +Gamecenter launchers and theme settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `gamecenter.fastfood.apiKey` | `` | `string` | API key used by the FastFood or BaseJump integration. | +| `gamecenter.fastfood.assets` | `http://localhost/swf/c_images/gamecenter_basejump/` | `url` | Asset base URL used by the FastFood or BaseJump game client. | +| `gamecenter.fastfood.background.color` | `68bbd2` | `string` | Background color used by the FastFood launcher UI. | +| `gamecenter.fastfood.enabled` | `true` | `boolean` | Enable the FastFood or BaseJump gamecenter integration. | +| `gamecenter.fastfood.text.color` | `ffffff` | `string` | Text color used by the FastFood launcher UI. | +| `gamecenter.fastfood.theme` | `default` | `string` | Theme name used by the FastFood launcher. | +| `gamecenter.snowwar.artic.bg` | `http://localhost/swf/c_images/gamecenter_snowwar/snst_bg_1_a_big.png` | `url` | Background image used for the SnowWar Arctic map. | +| `gamecenter.snowwar.assets` | `http://localhost/swf/c_images/gamecenter_snowwar/` | `url` | Asset base URL used by the SnowWar game client. | +| `gamecenter.snowwar.dragoncave.bg` | `http://localhost/swf/c_images/gamecenter_snowwar/snst_bg_2_big.png` | `url` | Background image used for the SnowWar Dragon Cave map. | +| `gamecenter.snowwar.enabled` | `true` | `boolean` | Enable the SnowWar gamecenter integration. | +| `gamecenter.snowwar.fightnight.bg` | `http://localhost/swf/c_images/gamecenter_snowwar/snst_bg_3_noscale.png` | `url` | Background image used for the SnowWar Fight Night map. | +| `gamecenter.snowwar.game.background.color` | `93d4f3` | `string` | Background color used by the SnowWar launcher UI. | +| `gamecenter.snowwar.game.start.time` | `15` | `integer` | Countdown in seconds before a SnowWar round starts. | +| `gamecenter.snowwar.game.text.color` | `000000` | `integer` | Text color used by the SnowWar launcher UI. | +| `gamecenter.snowwar.players.min` | `2` | `integer` | Minimum number of players required to start SnowWar. | +| `gamecenter.snowwar.room.id` | `0` | `boolean` | Room ID used as the SnowWar lobby or host room. | + +## `gamedata` + +Remote gamedata sources. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `gamedata.figuredata.url` | `https://habbo.com/gamedata/figuredata/0` | `url` | Remote figuredata URL used when the hotel loads avatar figure definitions. | + +## `guardians` + +Guardians and report-review settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `guardians.accept.timer` | `90` | `integer` | Time in seconds that guardians have to accept a case. | +| `guardians.maximum.guardians.total` | `10` | `integer` | Maximum number of guardians that can be assigned to one case. | +| `guardians.maximum.resends` | `2` | `integer` | Maximum number of times an unanswered guardian case can be resent. | +| `guardians.minimum.votes` | `5` | `integer` | Minimum number of guardian votes required to resolve a case. | +| `guardians.reporting.cooldown` | `900` | `integer` | Cooldown in seconds before the same user can open a new guardian report. | + +## `hotel` + +Core hotel gameplay, economy, room, catalog and moderation settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `hotel.alert.oldstyle` | `0` | `boolean` | Use the legacy generic alert window style. | +| `hotel.allow.ignore.staffs` | `1` | `boolean` | Allow users to ignore staff accounts. | +| `hotel.auto.credits.amount` | `100` | `integer` | Amount of credits granted on each automatic payout. | +| `hotel.auto.credits.enabled` | `1` | `boolean` | Enable automatic credits payouts. | +| `hotel.auto.credits.hc_modifier` | `1` | `boolean` | Multiplier applied to automatic credits payouts for HC users. | +| `hotel.auto.credits.ignore.hotelview` | `1` | `boolean` | Skip users staying in hotel view when giving automatic credits payouts. | +| `hotel.auto.credits.ignore.idled` | `0` | `boolean` | Skip idle users when giving automatic credits payouts. | +| `hotel.auto.credits.interval` | `600` | `integer` | Interval in seconds between automatic credits payouts. | +| `hotel.auto.gotwpoints.enabled` | `0` | `boolean` | Enable automatic gotwpoints payouts. | +| `hotel.auto.gotwpoints.hc_modifier` | `1` | `boolean` | Multiplier applied to automatic gotwpoints payouts for HC users. | +| `hotel.auto.gotwpoints.ignore.hotelview` | `1` | `boolean` | Skip users staying in hotel view when giving automatic gotwpoints payouts. | +| `hotel.auto.gotwpoints.ignore.idled` | `1` | `boolean` | Skip idle users when giving automatic gotwpoints payouts. | +| `hotel.auto.gotwpoints.interval` | `600` | `integer` | Interval in seconds between automatic gotwpoints payouts. | +| `hotel.auto.gotwpoints.name` | `shell` | `string` | Internal currency name used by the automatic gotwpoints payout. | +| `hotel.auto.gotwpoints.type` | `4` | `integer` | Currency type ID used by the automatic gotwpoints payout. | +| `hotel.auto.pixels.amount` | `100` | `integer` | Amount of pixels granted on each automatic payout. | +| `hotel.auto.pixels.enabled` | `1` | `boolean` | Enable automatic pixels payouts. | +| `hotel.auto.pixels.hc_modifier` | `1` | `boolean` | Multiplier applied to automatic pixels payouts for HC users. | +| `hotel.auto.pixels.ignore.hotelview` | `1` | `boolean` | Skip users staying in hotel view when giving automatic pixels payouts. | +| `hotel.auto.pixels.ignore.idled` | `1` | `boolean` | Skip idle users when giving automatic pixels payouts. | +| `hotel.auto.pixels.interval` | `600` | `integer` | Interval in seconds between automatic pixels payouts. | +| `hotel.auto.points.amount` | `5` | `integer` | Amount of points granted on each automatic payout. | +| `hotel.auto.points.enabled` | `1` | `boolean` | Enable automatic points payouts. | +| `hotel.auto.points.hc_modifier` | `1` | `boolean` | Multiplier applied to automatic points payouts for HC users. | +| `hotel.auto.points.ignore.hotelview` | `0` | `boolean` | Skip users staying in hotel view when giving automatic points payouts. | +| `hotel.auto.points.ignore.idled` | `0` | `boolean` | Skip idle users when giving automatic points payouts. | +| `hotel.auto.points.interval` | `600` | `integer` | Interval in seconds between automatic points payouts. | +| `hotel.banzai.points.tile.fill` | `0` | `boolean` | Configuration value used by `hotel.banzai.points.tile.fill`. | +| `hotel.banzai.points.tile.lock` | `1` | `boolean` | Configuration value used by `hotel.banzai.points.tile.lock`. | +| `hotel.banzai.points.tile.steal` | `0` | `boolean` | Configuration value used by `hotel.banzai.points.tile.steal`. | +| `hotel.bot.butler.commanddistance` | `5` | `integer` | Maximum tile distance from which a butler bot accepts commands. | +| `hotel.bot.butler.servedistance` | `5` | `integer` | Maximum tile distance from which a butler bot can serve requests. | +| `hotel.bot.chat.minimum.interval` | `5` | `integer` | Minimum number of seconds between bot chat lines. | +| `hotel.bot.max.chatdelay` | `604800` | `integer` | Maximum bot chat delay allowed when configuring scripted speech. | +| `hotel.bot.max.chatlength` | `120` | `integer` | Maximum number of characters allowed in bot chat lines. | +| `hotel.bot.max.namelength` | `15` | `integer` | Maximum number of characters allowed in bot names. | +| `hotel.bots.max.inventory` | `25` | `integer` | Maximum number of bots allowed in one inventory. | +| `hotel.bots.max.room` | `10` | `integer` | Maximum number of bots allowed in one room. | +| `hotel.calendar.default` | `test` | `string` | Default calendar campaign name or identifier. | +| `hotel.calendar.enabled` | `0` | `boolean` | Enable the hotel calendar feature. | +| `hotel.calendar.pixels.hc_modifier` | `2.0` | `number` | Multiplier applied to calendar pixel rewards for HC users. | +| `hotel.calendar.starttimestamp` | `1593561600` | `integer` | Unix timestamp used as the calendar start date. | +| `hotel.catalog.discounts.amount` | `6` | `integer` | Number of discount slots or discount batches shown by the catalog. | +| `hotel.catalog.items.display.ordernum` | `1` | `boolean` | Respect catalog item order numbers when rendering pages. | +| `hotel.catalog.ltd.limit.enabled` | `1` | `boolean` | Enable daily purchase limits for limited catalog items. | +| `hotel.catalog.purchase.cooldown` | `1` | `boolean` | Cooldown in seconds between catalog purchases. | +| `hotel.catalog.recycler.enabled` | `1` | `boolean` | Enable the catalog recycler feature. | +| `hotel.chat.max.length` | `100` | `integer` | Maximum number of characters allowed in one public chat message. | +| `hotel.daily.respect` | `3` | `integer` | Daily amount of respect points available for users. | +| `hotel.daily.respect.pets` | `3` | `integer` | Daily amount of pet respect points available for users. | +| `hotel.ecotron.enabled` | `1` | `boolean` | Enable or disable the feature controlled by `hotel.ecotron.enabled`. | +| `hotel.ecotron.rarity.chance.1` | `1` | `boolean` | Configuration value used by `hotel.ecotron.rarity.chance.1`. | +| `hotel.ecotron.rarity.chance.2` | `4` | `integer` | Configuration value used by `hotel.ecotron.rarity.chance.2`. | +| `hotel.ecotron.rarity.chance.3` | `40` | `integer` | Configuration value used by `hotel.ecotron.rarity.chance.3`. | +| `hotel.ecotron.rarity.chance.4` | `200` | `integer` | Configuration value used by `hotel.ecotron.rarity.chance.4`. | +| `hotel.ecotron.rarity.chance.5` | `2000` | `integer` | Configuration value used by `hotel.ecotron.rarity.chance.5`. | +| `hotel.flood.mute.time` | `30` | `integer` | Mute duration in seconds applied by the hotel flood protection. | +| `hotel.floorplan.max.totalarea` | `4096` | `integer` | Maximum total floorplan area allowed for custom rooms. | +| `hotel.floorplan.max.widthlength` | `64` | `integer` | Maximum floorplan width or length allowed for custom rooms. | +| `hotel.freeze.onfreeze.loose.explosionboost` | `3` | `integer` | Number of explosion boosts lost when a player gets frozen. | +| `hotel.freeze.onfreeze.loose.snowballs` | `5` | `integer` | Number of snowballs lost when a player gets frozen. | +| `hotel.freeze.onfreeze.time.frozen` | `5` | `integer` | Time in seconds a player remains frozen. | +| `hotel.freeze.points.block` | `1` | `boolean` | Score awarded for blocking tiles in Freeze. | +| `hotel.freeze.points.effect` | `3` | `integer` | Score awarded for using Freeze effects or power-up actions. | +| `hotel.freeze.points.freeze` | `10` | `integer` | Score awarded for freezing another player in Freeze. | +| `hotel.freeze.powerup.chance` | `33` | `integer` | Chance for Freeze power-ups to spawn. | +| `hotel.freeze.powerup.max.lives` | `3` | `integer` | Maximum number of extra lives granted by a Freeze power-up. | +| `hotel.freeze.powerup.max.snowballs` | `5` | `integer` | Maximum number of extra snowballs granted by a Freeze power-up. | +| `hotel.freeze.powerup.protection.stack` | `1` | `boolean` | Allow Freeze protection power-ups to stack. | +| `hotel.freeze.powerup.protection.time` | `10` | `integer` | Protection time in seconds after receiving a Freeze protection power-up. | +| `hotel.friendcategory` | `0` | `boolean` | Default friend category ID assigned to new friends. | +| `hotel.furni.gym.achievement.olympics_c16_crosstrainer` | `CrossTrainer` | `string` | Configuration value used by `hotel.furni.gym.achievement.olympics_c16_crosstrainer`. | +| `hotel.furni.gym.achievement.olympics_c16_trampoline` | `Trampolinist` | `string` | Configuration value used by `hotel.furni.gym.achievement.olympics_c16_trampoline`. | +| `hotel.furni.gym.achievement.olympics_c16_treadmill` | `Jogger` | `string` | Configuration value used by `hotel.furni.gym.achievement.olympics_c16_treadmill`. | +| `hotel.furni.gym.forcerot.olympics_c16_crosstrainer` | `1` | `boolean` | Configuration value used by `hotel.furni.gym.forcerot.olympics_c16_crosstrainer`. | +| `hotel.furni.gym.forcerot.olympics_c16_trampoline` | `0` | `boolean` | Configuration value used by `hotel.furni.gym.forcerot.olympics_c16_trampoline`. | +| `hotel.furni.gym.forcerot.olympics_c16_treadmill` | `1` | `boolean` | Configuration value used by `hotel.furni.gym.forcerot.olympics_c16_treadmill`. | +| `hotel.gifts.box_types` | `0,1,2,3,4,5,6,8` | `list` | Comma-separated list of gift box type IDs allowed in the catalog. | +| `hotel.gifts.length.max` | `300` | `integer` | Maximum message length allowed on gift notes. | +| `hotel.gifts.ribbon_types` | `0,1,2,3,4,5,6,7,8,9,10` | `list` | Comma-separated list of ribbon type IDs allowed in the catalog. | +| `hotel.gifts.special.price` | `10` | `integer` | Credit price used by special gift boxes. | +| `hotel.home.room` | `0` | `boolean` | Room ID used as the default home room for new users. | +| `hotel.inventory.max.items` | `7500` | `integer` | Maximum number of items allowed in one inventory. | +| `hotel.item.trap.hween14_rare2` | `3000` | `integer` | Configuration value used by `hotel.item.trap.hween14_rare2`. | +| `hotel.item.trap.hween_c17_handstrap` | `3000` | `integer` | Configuration value used by `hotel.item.trap.hween_c17_handstrap`. | +| `hotel.item.trap.hween_c17_spiketrap` | `3000` | `integer` | Configuration value used by `hotel.item.trap.hween_c17_spiketrap`. | +| `hotel.item.trap.pirate_sandtrap` | `3000` | `integer` | Configuration value used by `hotel.item.trap.pirate_sandtrap`. | +| `hotel.jukebox.limit.large` | `20` | `integer` | Track limit used by large jukebox furniture. | +| `hotel.jukebox.limit.normal` | `10` | `integer` | Track limit used by normal jukebox furniture. | +| `hotel.log.chat` | `1` | `boolean` | Enable logging for chat. | +| `hotel.log.chat.private` | `1` | `boolean` | Enable logging for chat private. | +| `hotel.log.room.enter` | `1` | `boolean` | Enable logging for room enter. | +| `hotel.log.trades` | `1` | `boolean` | Enable logging for trades. | +| `hotel.marketplace.currency` | `0` | `boolean` | Currency type used for marketplace prices and taxes. | +| `hotel.marketplace.enabled` | `1` | `boolean` | Enable or disable the feature controlled by `hotel.marketplace.enabled`. | +| `hotel.max.bots.room` | `10` | `integer` | Maximum number of bots allowed in one room. | +| `hotel.max.duckets` | `9000000` | `integer` | Maximum amount of duckets a user can hold. | +| `hotel.messenger.offline.messaging.enabled` | `1` | `boolean` | Enable or disable the feature controlled by `hotel.messenger.offline.messaging.enabled`. | +| `hotel.messenger.search.maxresults` | `50` | `integer` | Maximum number of results returned by messenger user searches. | +| `hotel.name` | `Habbo Hotel` | `string` | Public hotel name shown across the client and outgoing messages. | +| `hotel.navigator.camera` | `1` | `boolean` | Enable navigator room previews or camera mode. | +| `hotel.navigator.owner` | `HabboHotel` | `string` | Default owner name displayed by the navigator. | +| `hotel.navigator.popular.amount` | `35` | `integer` | Number of rooms shown in the popular rooms list. | +| `hotel.navigator.popular.category.maxresults` | `10` | `integer` | Maximum number of rooms shown per popular category. | +| `hotel.navigator.popular.listtype` | `1` | `boolean` | List type used for the popular rooms tab. | +| `hotel.navigator.populartab.publics` | `0` | `boolean` | Include public rooms inside the popular rooms tab. | +| `hotel.navigator.search.maxresults` | `35` | `integer` | Maximum number of results returned by navigator searches. | +| `hotel.navigator.sort.ordernum` | `1` | `boolean` | Respect order numbers when sorting navigator results. | +| `hotel.navigator.staffpicks.categoryid` | `1` | `boolean` | Category ID used for the staff picks tab. | +| `hotel.nux.gifts.enabled` | `0` | `boolean` | Enable the NUX gift flow for new users. | +| `hotel.pets.max.inventory` | `25` | `integer` | Maximum number of pets allowed in one inventory. | +| `hotel.pets.max.room` | `10` | `integer` | Maximum number of pets allowed in one room. | +| `hotel.pets.name.length.max` | `15` | `integer` | Maximum pet name length. | +| `hotel.pets.name.length.min` | `3` | `integer` | Minimum pet name length. | +| `hotel.player.name` | `Habbo` | `string` | Generic player label used by text templates and client messages. | +| `hotel.purchase.ltd.limit.daily.item` | `3` | `integer` | Maximum number of the same limited item a user can buy per day. | +| `hotel.purchase.ltd.limit.daily.total` | `10` | `integer` | Maximum number of limited items a user can buy per day across all limited sales. | +| `hotel.refill.daily` | `86400` | `integer` | Cooldown in seconds before daily counters such as respect are refilled. | +| `hotel.rollers.speed.maximum` | `100` | `integer` | Maximum roller delay or speed value accepted by roller furniture. | +| `hotel.room.enter.logs` | `1` | `boolean` | Enable room-entry logs. | +| `hotel.room.floorplan.check.enabled` | `1` | `boolean` | Validate custom floorplans before rooms are saved. | +| `hotel.room.furni.max` | `2500` | `integer` | Maximum amount of furniture allowed in one room. | +| `hotel.room.nooblobby` | `3` | `integer` | Room ID used as the newbie lobby. | +| `hotel.room.public.doortile.kick` | `0` | `boolean` | Kick users who stand on public room door tiles. | +| `hotel.room.rollers.norules` | `0` | `boolean` | Allow rollers to ignore normal placement rules. | +| `hotel.room.rollers.roll_avatars.max` | `1` | `boolean` | Maximum number of avatars that rollers can move at once. | +| `hotel.room.stickies.max` | `200` | `integer` | Maximum number of sticky notes allowed in one room. | +| `hotel.room.stickypole.prefix` | `%timestamp%, %username%:\\r` | `template` | Prefix template written by sticky pole furniture. | +| `hotel.room.tags.staff` | `staff;official;habbo` | `list` | Semicolon-separated staff room tags. | +| `hotel.rooms.auto.idle` | `1` | `boolean` | Allow empty rooms to switch into the idle state automatically. | +| `hotel.rooms.deco_hosting` | `1` | `boolean` | Enable decoration-hosting features for rooms. | +| `hotel.rooms.handitem.time` | `100` | `integer` | Time in seconds before temporary hand items are cleared. | +| `hotel.rooms.max.favorite` | `30` | `integer` | Maximum number of favorite rooms allowed per user. | +| `hotel.roomuser.idle.cycles` | `300` | `integer` | Idle cycle count before a room user is marked idle. | +| `hotel.roomuser.idle.cycles.kick` | `900` | `integer` | Idle cycle count before a room user is kicked for idling. | +| `hotel.roomuser.idle.not_dancing.ignore.wired_idle` | `0` | `boolean` | Ignore the wired idle status when checking the room idle rule. | +| `hotel.sanctions.enabled` | `1` | `boolean` | Enable the sanctions system. | +| `hotel.shop.discount.modifier` | `6` | `integer` | Modifier used by the shop discount calculation. | +| `hotel.talenttrack.enabled` | `1` | `boolean` | Enable the talent track feature. | +| `hotel.targetoffer.id` | `1` | `boolean` | Offer ID requested when the client asks for a targeted offer. | +| `hotel.teleport.locked.allowed` | `1` | `boolean` | Allow users to use teleports inside locked rooms when they otherwise qualify. | +| `hotel.trading.enabled` | `1` | `boolean` | Enable room trading. | +| `hotel.trading.requires.perk` | `0` | `boolean` | Require the trading perk before users may trade. | +| `hotel.trophies.length.max` | `300` | `integer` | Maximum value used by `hotel.trophies.length.max`. | +| `hotel.users.clothingvalidation.onchangelooks` | `0` | `boolean` | Run clothing validation when the related action occurs: onchangelooks. | +| `hotel.users.clothingvalidation.onfballgate` | `0` | `boolean` | Run clothing validation when the related action occurs: onfballgate. | +| `hotel.users.clothingvalidation.onhcexpired` | `0` | `boolean` | Run clothing validation when the related action occurs: onhcexpired. | +| `hotel.users.clothingvalidation.onlogin` | `0` | `boolean` | Run clothing validation when the related action occurs: onlogin. | +| `hotel.users.clothingvalidation.onmannequin` | `0` | `boolean` | Run clothing validation when the related action occurs: onmannequin. | +| `hotel.users.clothingvalidation.onmimic` | `0` | `boolean` | Run clothing validation when the related action occurs: onmimic. | +| `hotel.users.max.friends` | `300` | `integer` | Maximum number of friends allowed for normal users. | +| `hotel.users.max.friends.hc` | `1100` | `integer` | Maximum number of friends allowed for HC users. | +| `hotel.users.max.rooms` | `50` | `integer` | Maximum number of rooms allowed for normal users. | +| `hotel.users.max.rooms.hc` | `75` | `integer` | Maximum number of rooms allowed for HC users. | +| `hotel.view.ltdcountdown.enabled` | `1` | `boolean` | Enable the limited-countdown hotel-view widget. | +| `hotel.view.ltdcountdown.itemid` | `10388` | `integer` | Item ID shown by the limited-countdown widget. | +| `hotel.view.ltdcountdown.itemname` | `trophy_netsafety_0` | `string` | Item name shown by the limited-countdown widget. | +| `hotel.view.ltdcountdown.pageid` | `13` | `integer` | Catalog page ID linked by the limited-countdown widget. | +| `hotel.view.ltdcountdown.timestamp` | `1519496132` | `integer` | Unix timestamp used by the limited-countdown widget. | +| `hotel.welcome.alert.delay` | `10000` | `integer` | Delay in milliseconds before the welcome alert is shown. | +| `hotel.welcome.alert.enabled` | `0` | `boolean` | Enable the welcome alert shown after login. | +| `hotel.welcome.alert.message` | `Welcome to Habbo Hotel %user%!` | `template` | Message template used by the welcome alert. | +| `hotel.welcome.alert.oldstyle` | `0` | `boolean` | Use the legacy welcome alert window style. | +| `hotel.wordfilter.automute` | `1` | `boolean` | Mute duration in minutes applied when word-filter automute is triggered. | +| `hotel.wordfilter.enabled` | `1` | `boolean` | Enable the word filter system. | +| `hotel.wordfilter.messenger` | `1` | `boolean` | Apply the word filter to messenger messages. | +| `hotel.wordfilter.normalise` | `1` | `boolean` | Normalise text before checking it against the word filter. | +| `hotel.wordfilter.replacement` | `bobba` | `string` | Replacement word used when text is censored. | +| `hotel.wordfilter.rooms` | `1` | `boolean` | Apply the word filter to room chat. | + +## `hotelview` + +Hotel-view widgets and promotional data. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `hotelview.halloffame.query` | `SELECT users.look, users.username, users.id, users_settings.hof_points FROM users_settings INNER JOIN users ON users_settings.user_id = users.id WHERE hof_points > 0 ORDER BY hof_points DESC, users.id ASC LIMIT 10` | `sql` | SQL query used to populate the hotel-view hall of fame panel. | +| `hotelview.promotional.points` | `100` | `integer` | Amount of activity points awarded by the hotel-view promotion. | +| `hotelview.promotional.points.type` | `5` | `integer` | Activity point type used by the hotel-view promotional reward. | +| `hotelview.promotional.reward.id` | `11043` | `integer` | Base item ID used by the hotel-view promotional reward. | +| `hotelview.promotional.reward.name` | `bonusbag20_2` | `string` | Public item name used by the hotel-view promotional reward. | + +## `imager` + +Internal image generator paths and URLs. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `imager.internal.enabled` | `1` | `boolean` | Generate images locally instead of relying on an external imager service. | +| `imager.location.badgeparts` | `/var/www/testhotel/Cosmic/public/usercontent/badgeparts` | `string` | Filesystem path where badge part assets are stored. | +| `imager.location.output.badges` | `/var/www/testhotel/Cosmic/public/usercontent/badgeparts/generated/` | `string` | Filesystem output path for generated badges. | +| `imager.location.output.camera` | `/var/www/testhotel/Cosmic/public/usercontent/camera/` | `string` | Filesystem output path for saved camera photos. | +| `imager.location.output.thumbnail` | `/var/www/testhotel/Cosmic/public/usercontent/camera/thumbnail/` | `string` | Filesystem output path for generated camera thumbnails. | +| `imager.url.youtube` | `imager.php?url=http://img.youtube.com/vi/%video%/default.jpg` | `template` | Template URL used to fetch YouTube thumbnails. | + +## `images` + +Static client image path helpers. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `images.gamecenter.basejump` | `c_images/gamecenter_basejump/` | `string` | Client asset path used for the basejump gamecenter images. | +| `images.gamecenter.snowwar` | `c_images/gamecenter_snowwar/` | `string` | Client asset path used for the snowwar gamecenter images. | + +## `info` + +Global information panel toggle. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `info.shown` | `1` | `boolean` | Show the hotel information panel or startup information message. | + +## `invisible` + +Invisible-mode behaviour. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `invisible.prevent.chat` | `0` | `boolean` | Prevent invisible users from speaking in rooms. | + +## `io` + +Socket and Netty threading settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `io.bossgroup.threads` | `1` | `boolean` | Number of Netty boss-group threads used by the socket server. | +| `io.client.multithreaded.handler` | `1` | `boolean` | Handle incoming client packets with a multi-threaded pipeline. | +| `io.workergroup.threads` | `5` | `integer` | Number of Netty worker-group threads used by the socket server. | + +## `logging` + +Structured emulator logging toggles. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `logging.debug` | `0` | `boolean` | Enable extra debug logging in the emulator logger. | +| `logging.errors.packets` | `0` | `boolean` | Log packet parsing errors. | +| `logging.errors.runtime` | `1` | `boolean` | Log runtime exceptions. | +| `logging.errors.sql` | `1` | `boolean` | Log SQL errors. | +| `logging.packets` | `0` | `boolean` | Log packet traffic in the standard logger. | +| `logging.packets.undefined` | `0` | `boolean` | Log undefined packets in the standard logger. | + +## `marketplace` + +Marketplace compatibility flag. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `marketplace.enabled` | `1` | `boolean` | Global switch for the marketplace subsystem. | + +## `monsterplant` + +Monster plant seed item mapping. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `monsterplant.seed.item_id` | `4582` | `integer` | Configuration value used by `monsterplant.seed.item_id`. | +| `monsterplant.seed_rare.item_id` | `4604` | `integer` | Configuration value used by `monsterplant.seed_rare.item_id`. | + +## `moodlight` + +Moodlight validation switches. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `moodlight.color_check.enabled` | `1` | `boolean` | Validate moodlight color values before applying them. | + +## `navigator` + +Navigator static definitions. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `navigator.eventcategories` | `1,Hottest Events,false;2,Parties & Music,true;3,Role Play,true;4,Help Desk,true;5,Trading,true;6,Games,true;7,Debates & Discussions,true;8,Grand Openings,true;9,Friending,true;10,Jobs,true;11,Group Events,true` | `list` | Semicolon-separated navigator event category definitions shown in the events tab. | + +## `networking` + +Low-level networking compatibility switches. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `networking.tcp.proxy` | `0` | `boolean` | Enable TCP proxy-aware networking behaviour. | + +## `notify` + +Server-side notification automation. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `notify.staff.chat.auto.report` | `1` | `boolean` | Automatically notify staff when a chat report is created. | + +## `path` + +Asset path helpers. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `path.furniture.icons` | `${image.library.url}/icons/` | `template` | Base path used by the client to load furniture icon assets. | + +## `pathfinder` + +Pathfinder safety and performance settings. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `pathfinder.execution_time.milli` | `25` | `integer` | Maximum pathfinder execution time in milliseconds before aborting. | +| `pathfinder.max_execution_time.enabled` | `1` | `boolean` | Enforce the pathfinder execution time limit. | +| `pathfinder.step.allow.falling` | `1` | `boolean` | Allow the pathfinder to walk down falling steps. | +| `pathfinder.step.maximum.height` | `1.1` | `number` | Maximum height difference the pathfinder may step onto. | + +## `pirate_parrot` + +Pirate parrot text and bubble behaviour. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `pirate_parrot.message.bubble` | `28` | `integer` | Chat bubble style ID used by the pirate parrot. | +| `pirate_parrot.message.count` | `6` | `integer` | Number of predefined messages available to the pirate parrot. | + +## `postit` + +Post-it constraints. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `postit.charlimit` | `366` | `integer` | Maximum number of characters allowed on post-it notes. | + +## `pyramids` + +Pyramids minigame timing. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `pyramids.max.delay` | `18` | `integer` | Maximum delay allowed in the Pyramids minigame or puzzle timing. | + +## `retro` + +Retro compatibility switches. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `retro.style.homeroom` | `1` | `boolean` | Use retro-style home room behaviour in the navigator or onboarding flow. | + +## `room` + +Generic room chat and promotion behaviour. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `room.chat.delay` | `0` | `boolean` | Extra room chat delay applied before users can speak again. | +| `room.chat.mutearea.allow_whisper` | `1` | `boolean` | Allow whispering while a user stands inside a mute area. | +| `room.chat.prefix.format` | `[%prefix%] ` | `string` | HTML or text format used for room chat prefixes. | +| `room.promotion.badge` | `RADZZ` | `string` | Badge code displayed on promoted rooms. | + +## `rosie` + +Rosie-related client notifications and purchase currency. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `rosie.bubble.image.url` | `${image.library.url}notifications/generic.png` | `template` | Image used by Rosie bubble notifications. | +| `rosie.buyroom.currency.type` | `5` | `integer` | Currency type used by Rosie when buying a room or room package. | + +## `runtime` + +Executor and thread sizing. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `runtime.threads` | `8` | `integer` | Configuration value used by `runtime.threads`. | + +## `save` + +Chat persistence toggles. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `save.private.chats` | `1` | `boolean` | Configuration value used by `save.private.chats`. | +| `save.room.chats` | `1` | `boolean` | Configuration value used by `save.room.chats`. | + +## `scripter` + +Scripter or modtool integration. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `scripter.modtool.tickets` | `1` | `boolean` | Expose moderation tickets to the scripter or automation tooling. | + +## `seasonal` + +Seasonal currency mapping. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `seasonal.currency.diamond` | `5` | `integer` | Currency type ID used for diamonds. | +| `seasonal.currency.ducket` | `0` | `boolean` | Currency type ID used for duckets. | +| `seasonal.currency.names` | `ducket;pixel;shell;diamond` | `list` | Semicolon-separated display names for seasonal currency types. | +| `seasonal.currency.pixel` | `0` | `boolean` | Currency type ID used for pixels. | +| `seasonal.currency.shell` | `4` | `integer` | Currency type ID used for shells. | +| `seasonal.primary.type` | `5` | `integer` | Primary seasonal currency type ID. | +| `seasonal.types` | `0;1;2;3;4;5;101;102;103;104;105` | `list` | Semicolon-separated list of currency type IDs treated as seasonal currencies. | + +## `subscriptions` + +HC scheduler, payday and discount configuration. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `subscriptions.hc.achievement` | `VipHC` | `string` | Achievement code granted for the HC subscription tier. | +| `subscriptions.hc.discount.days_before_end` | `7` | `integer` | Number of days before expiry when HC discount offers become available. | +| `subscriptions.hc.discount.enabled` | `1` | `boolean` | Enable discounted HC renewal offers. | +| `subscriptions.hc.payday.creditsspent_reset_on_expire` | `1` | `boolean` | Reset tracked credits spent when the HC subscription expires. | +| `subscriptions.hc.payday.currency` | `credits` | `string` | Currency rewarded by the HC payday system. | +| `subscriptions.hc.payday.enabled` | `1` | `boolean` | Enable the HC payday reward system. | +| `subscriptions.hc.payday.interval` | `1 month` | `string` | Date interval used between HC payday reward runs. | +| `subscriptions.hc.payday.next_date` | `2020-10-15 00:00:00` | `string` | Next scheduled execution date for HC payday rewards. | +| `subscriptions.hc.payday.percentage` | `10` | `integer` | Percentage of eligible spending returned by HC payday. | +| `subscriptions.hc.payday.streak` | `7=5;30=10;60=15;90=20;180=25;365=30` | `list` | Semicolon-separated streak thresholds and rewards for HC payday. | +| `subscriptions.scheduler.enabled` | `1` | `boolean` | Enable the subscription background scheduler. | +| `subscriptions.scheduler.interval` | `10` | `integer` | Interval in minutes between subscription scheduler runs. | + +## `team` + +Compatibility markers for team or wired integrations. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `team.wired.update.rc-1` | `DO NOT REMOVE THIS SETTING!` | `string` | Compatibility marker used by the custom team wired implementation. Do not remove. | + +## `youtube` + +YouTube integration credentials. + +| Key | Default value | Type | Purpose | +|---|---|---|---| +| `youtube.apikey` | `` | `string` | API key used by the YouTube integration. | + diff --git a/docs/permissions_schema_reference.md b/docs/permissions_schema_reference.md new file mode 100644 index 00000000..58fabd33 --- /dev/null +++ b/docs/permissions_schema_reference.md @@ -0,0 +1,187 @@ +# Permissions schema reference + +## Overview + +The legacy `permissions` table stores: + +- one row per rank +- one column per permission key + +That works for runtime, but it becomes very hard to read and maintain once the number of permission keys grows. + +The updated design keeps only the rank metadata separated, while the permission matrix itself becomes one readable table: + +- `permission_ranks` + - one row per rank + - stores rank metadata such as `rank_name`, `badge`, `level`, `prefix`, `room_effect`, and the automatic currency amounts +- `permission_definitions` + - one row per permission key + - stores the permission comment in the same row + - stores one column per rank using the format `rank_` + +Example: + +| permission_key | max_value | comment | rank_1 | rank_2 | rank_7 | +| --- | --- | --- | --- | --- | --- | +| `acc_ads_background` | `1` | Allows editing room advertisement backgrounds. | `0` | `0` | `1` | + +## Runtime behavior + +- The emulator still supports the legacy `permissions` table as a fallback. +- If `permission_ranks` and `permission_definitions` exist and contain data, the emulator loads the new schema instead. +- If the new schema is missing, incomplete, or fails to load, the emulator falls back to the legacy `permissions` table automatically. + +Relevant runtime files: + +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionsManager.java:45` +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Rank.java:71` +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java:57` + +## Tables + +### `permission_ranks` + +This table stores only rank metadata: + +- `id` +- `rank_name` +- `hidden_rank` +- `badge` +- `job_description` +- `staff_color` +- `staff_background` +- `level` +- `room_effect` +- `log_commands` +- `prefix` +- `prefix_color` +- `auto_credits_amount` +- `auto_pixels_amount` +- `auto_gotw_amount` +- `auto_points_amount` + +#### `permission_ranks` field meanings + +- `id` + - Numeric rank id used everywhere else in the emulator, including `users.rank` and the dynamic `rank_` columns in `permission_definitions`. +- `rank_name` + - Human-readable name of the rank, such as `User`, `Moderator`, or `Administrator`. +- `hidden_rank` + - When enabled, the rank is treated as hidden in places where staff visibility should be reduced. +- `badge` + - Badge code automatically associated with the rank. +- `job_description` + - Staff/job description text shown in features that expose rank profile details. +- `staff_color` + - Hex color used by staff UI or visuals that depend on the rank color. +- `staff_background` + - Background asset name used for staff visuals tied to the rank. +- `level` + - Priority/order value of the rank; higher values usually mean stronger staff level or broader access. +- `room_effect` + - Default avatar effect id associated with the rank when that feature is used. +- `log_commands` + - Controls whether commands executed by users with this rank should be logged in command logs. +- `prefix` + - Short in-room staff prefix/tag associated with the rank. +- `prefix_color` + - Hex color used for the displayed rank prefix. +- `auto_credits_amount` + - Automatic credit amount granted by rank-based reward/payday style logic, if used by the hotel. +- `auto_pixels_amount` + - Automatic duckets/pixels amount granted by rank-based reward/payday style logic, if used by the hotel. +- `auto_gotw_amount` + - Automatic GOTW-style points amount granted by rank-based reward/payday style logic, if used by the hotel. +- `auto_points_amount` + - Automatic activity-points amount granted by rank-based reward/payday style logic, if used by the hotel. + +### `permission_definitions` + +This table stores: + +- `permission_key` +- `max_value` +- `comment` +- one dynamic column per rank: + - `rank_1` + - `rank_2` + - `rank_3` + - ... + +That means the table itself is already the readable matrix you wanted: + +- rows = permission keys +- columns = rank values +- comment stays next to the key + +## Value semantics + +Permission values keep the same meaning as today: + +- `0` = disabled +- `1` = allowed +- `2` = allowed only when room-owner rights may be used + +The schema stores that information in: + +- `permission_definitions.max_value` + +## Migration behavior + +`Database Updates/004_normalize_permissions_schema.sql` does the following: + +1. keeps the legacy `permissions` table untouched +2. creates `permission_ranks` +3. creates `permission_definitions` +4. copies rank metadata from `permissions` into `permission_ranks` +5. creates any missing `rank_` columns in `permission_definitions` +6. creates one row per permission key with `max_value` and a comment +7. applies curated per-key comments so every permission explains what it actually does in code +8. copies each old permission value into the proper `rank_` column + +It also removes the older experimental objects if they already exist: + +- `permission_rank_values` +- `permission_nodes` +- `permissions_matrix_view` +- `refresh_permissions_matrix_view` + +## Adding a new rank later + +When you add a new rank after the migration: + +1. insert the rank metadata into `permission_ranks` +2. reload permissions with emulator restart or `:update_permissions` +3. the emulator automatically creates the missing `rank_` column in `permission_definitions` if it does not exist yet +4. set the new `rank_` values in `permission_definitions` + +You can still run the helper procedure manually if you want to sync the schema before the next reload: + +```sql +CALL refresh_permission_definition_rank_columns(); +``` + +If you want to refresh all values again from the old legacy table during rollout, you can also run: + +```sql +CALL refresh_permission_definition_values(); +``` + +## Notes about comments and legacy keys + +The comments stored in `permission_definitions.comment` are intentionally hand-curated. + +- Where a Java handler exists, the comment follows the real runtime behavior. +- Where only legacy command texts exist, the comment marks the key as legacy and explains the intended behavior from those texts. +- Where a key is still present for compatibility but no direct handler is found in the current tree, the comment says so explicitly. + +The new schema intentionally preserves legacy and inconsistent permission keys so current functionality stays intact. + +Examples: + +- `cmd_word_quiz` +- `cmd_wordquiz` +- `cms_dance` +- `kiss_cmd` + +Those can be cleaned up later only after runtime behavior has been verified and the hotel no longer depends on the old names. diff --git a/docs/wired_bug_audit.md b/docs/wired_bug_audit.md new file mode 100644 index 00000000..9ad5f765 --- /dev/null +++ b/docs/wired_bug_audit.md @@ -0,0 +1,382 @@ +# Wired Bug Audit + +## 1. Scopo + +Questo documento raccoglie i **potenziali bug**, le **aree fragili** e le **incoerenze architetturali** emerse durante l’analisi del sistema wired. + +Non tutti i punti qui sotto sono bug già riprodotti al 100%, ma sono: + +- problemi già visti in comportamento reale +- incongruenze tra runtime e UI +- zone del codice che possono generare regressioni o risultati non deterministici + +Riferimenti principali: + +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java` +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java` +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java` +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredMoveCarryHelper.java` +- `Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired` + +--- + +## 2. Sintesi Priorità + +| Priorità | Tema | Stato | +|---|---|---| +| Alta | `context` variabili esposto ma non implementato davvero | Incoerenza forte | +| Alta | Doppio runtime (`WiredManager` vs `WiredHandler`) | Rischio architetturale | +| Alta | Ordine effect non sempre garantito senza extra esplicito | Rischio comportamentale | +| Alta | Path movimento legacy può ancora far trapelare update intermedi | Già osservato in stanza | +| Media | Tick a `50ms` ma delay wired in step da `500ms` | Semantica non uniforme | +| Media | Polling realtime `:wired` a `50ms` | Rischio carico/runtime noise | +| Media | `click furni` ora immediato, queue/cancel svuotati | Possibile regressione | +| Media | Semantica timestamp variabili non uniforme tra target types | Possibile confusione logica | + +--- + +## 3. Audit Dettagliato + +## 3.1 `context` nelle variabili: esposto ma non veramente supportato + +- **Gravità:** Alta +- **Confidenza:** Alta +- **Area:** effetti/condizioni/extras variabili + +### Problema + +Nel layout e in parte della serializzazione compare il target `context`, ma in più punti il runtime lo rifiuta esplicitamente oppure restituisce direttamente `false`. + +Questo crea una situazione pericolosa: + +- il designer pensa che la feature esista +- il box si salva o si configura parzialmente +- ma poi in esecuzione non produce il comportamento atteso + +### Evidenze + +- `WiredEffectGiveVariable.java:197` + - il save rifiuta `TARGET_CONTEXT` +- `WiredConditionVariableValueMatch.java:181` + - `case TARGET_CONTEXT -> false` +- `WiredConditionVariableAgeMatch.java:146` + - `case TARGET_CONTEXT -> false` +- `WiredExtraTextOutputVariable.java:83` + - il save rifiuta `TARGET_CONTEXT` + +### Impatto pratico + +- stack che sembrano validi in UI ma non funzionano a runtime +- falsi negativi nelle condition variabili +- placeholder testuali variabili non disponibili quando l’utente si aspetta il target context + +### Fix suggerito + +Scegliere una direzione netta: + +1. **o** implementare davvero `context` in tutti i flow variabili +2. **o** rimuoverlo completamente da UI, save e runtime finché non è pronto + +La seconda opzione è la più sicura nel breve periodo. + +--- + +## 3.2 Doppio runtime wired ancora presente + +- **Gravità:** Alta +- **Confidenza:** Alta +- **Area:** architettura core + +### Problema + +`WiredManager` dichiara di essere il runtime esclusivo e tratta i vecchi flag come sola compatibilità, ma `WiredHandler` esiste ancora con entrypoint completi e logica propria. + +### Evidenze + +- `WiredManager.java:136` + - warning esplicito: `wired.engine.enabled / wired.engine.exclusive are now compatibility-only flags` +- `WiredManager.java:174` + - `isEnabled()` dipende solo dall’inizializzazione del manager +- `WiredManager.java:182` + - `isExclusive()` ritorna sempre `true` +- `WiredHandler.java:63` + - entrypoint legacy completo `handle(...)` +- `WiredHandler.java:114` + - supporto separato per `handleCustomTrigger(...)` + +### Impatto pratico + +Se qualunque pezzo di codice, plugin o path legacy entra ancora in `WiredHandler`, si possono avere: + +- ordine effect diverso +- scheduling delay diverso +- condition flow diverso +- diagnostica/monitor non coerente col nuovo engine + +### Fix suggerito + +- definire un solo entrypoint runtime ufficiale +- se `WiredHandler` deve restare, trasformarlo in adapter minimo che inoltra sempre al nuovo engine +- aggiungere log o metriche per rilevare qualsiasi ingresso nel path legacy + +--- + +## 3.3 Ordine degli effect non sempre deterministico senza `wf_xtra_exec_in_order` + +- **Gravità:** Alta +- **Confidenza:** Alta +- **Area:** esecuzione stack + +### Problema + +Nel path legacy, l’ordinamento stabile viene applicato chiaramente solo in presenza di `wf_xtra_exec_in_order` oppure in casi specifici (`unseen`). + +Negli altri casi, l’ordine si appoggia alla collezione che arriva dal runtime. + +### Evidenze + +- `WiredHandler.java:224` + - rileva `hasExtraExecuteInOrder` +- `WiredHandler.java:230` + - ordina con `WiredExecutionOrderUtil.sort(effects)` solo in alcuni casi +- `WiredHandler.java:249` + - usa direttamente `effectList` in ordered mode + +### Impatto pratico + +Stack come: + +- `move_rotate` + `match_to_sshot` +- `toggle` + `reset` +- `give_var` + `change_var_val` + +possono produrre risultati diversi se si assume implicitamente un ordine che il runtime non promette davvero. + +### Fix suggerito + +- decidere se l’ordine stack deve essere sempre stabile di default +- in alternativa, mantenere la regola attuale ma documentarla in modo molto esplicito +- se si lascia la regola attuale, conviene segnalare in UI che l’ordine è garantito solo con `wf_xtra_exec_in_order` + +--- + +## 3.4 Il path movimento legacy può ancora far vedere movimenti intermedi + +- **Gravità:** Alta +- **Confidenza:** Alta +- **Area:** movement pipeline + +### Problema + +Il helper legacy di movimento usa ancora un fallback che, se il collector non è attivo, invia subito `FloorItemOnRollerComposer`. + +Questo può far trapelare al client uno stato intermedio che in teoria avrebbe dovuto essere nascosto da batching o restore finale. + +### Evidenze + +- `WiredMoveCarryHelper.java:163` + - metodo `moveFurniLegacy(...)` +- `WiredMoveCarryHelper.java:179` + - usa il collector se disponibile +- `WiredMoveCarryHelper.java:196` + - fallback diretto a `FloorItemOnRollerComposer` + +### Impatto pratico + +È coerente con il tipo di bug già visto: + +- oggetto che “si vede muovere” +- poi viene riportato nello stato corretto +- ma il client ha già ricevuto un update intermedio + +### Fix suggerito + +- evitare qualsiasi composer diretto nel path legacy quando la logica wired moderna è attiva +- centralizzare tutti i movement update in un unico collector finale +- aggiungere test specifici per: + - `move_rotate` + `match_to_sshot` + - stacked move effects nello stesso tick + +--- + +## 3.5 Tick a `50ms`, ma delay wired ancora a step da `500ms` + +- **Gravità:** Media +- **Confidenza:** Alta +- **Area:** semantica temporale + +### Problema + +Il sistema oggi ha due granularità temporali diverse: + +- repeaters / tickables a `50ms` +- delay wired classico a `delay * 500ms` + +### Evidenze + +- `WiredTickService.java:48` + - `DEFAULT_TICK_INTERVAL_MS = 50` +- `WiredTickService.java:175` + - `scheduleAtFixedRate(...)` +- `WiredEngine.java:753` + - `long delayMs = delay * 500L` +- `WiredHandler.java:369` + - stesso schema `delay * 500L` + +### Impatto pratico + +Non è per forza un bug, ma può creare: + +- aspettative sbagliate nel builder dei wired +- sensazione di desync tra repeater e delay +- stack “velocissimi” su tick ma “grossolani” sugli effect ritardati + +### Fix suggerito + +- o si accetta questa doppia semantica e la si documenta ovunque +- o si introduce una nuova famiglia di delay high-resolution separata dal delay classico + +--- + +## 3.6 `:wired` realtime a `50ms` può diventare rumoroso/pesante + +- **Gravità:** Media +- **Confidenza:** Alta +- **Area:** tooling monitor/inspection + +### Problema + +Le request di monitor e variabili ora sono rate-limitate a `50ms`. + +### Evidenze + +- `WiredMonitorRequestEvent.java:39` + - `return 50` +- `WiredUserVariablesRequestEvent.java:20` + - `return 50` + +### Impatto pratico + +Su una stanza attiva o con più client staff aperti: + +- carico rete maggiore +- più rumore sul server +- rischio di mascherare problemi reali con spam di refresh + +### Fix suggerito + +- spostare dove possibile a push/event driven +- lasciare `50ms` solo per il minimo indispensabile +- differenziare: + - monitor heavy/debug + - inspection live + - variables snapshot + +--- + +## 3.7 `click furni` ora è immediato: queue/cancel svuotati + +- **Gravità:** Media +- **Confidenza:** Alta +- **Area:** eventi click furni + +### Problema + +La queue dei click furni è stata semplificata: ora il click parte subito, e il cancel path è vuoto. + +### Evidenze + +- `WiredManager.java:274` + - `queueUserClicksFurni(...)` chiama subito `triggerUserClicksFurni(...)` +- `WiredManager.java:282` + - `cancelPendingUserClicksFurni(...)` non fa nulla + +### Impatto pratico + +Se qualche comportamento vecchio dipendeva da: + +- debounce +- cancel +- click differito + +ora può cambiare senza che il mapping sia ovvio. + +### Fix suggerito + +- decidere se il comportamento immediato è quello definitivo +- se sì, documentarlo come breaking behavior +- se no, reintrodurre una queue reale con semantica esplicita + +--- + +## 3.8 Semantica timestamp variabili non uniforme tra target type + +- **Gravità:** Media +- **Confidenza:** Media +- **Area:** sistema variabili + +### Problema + +Le variabili utente e furni hanno senso come “assegnazione con creation/update time”, mentre le room/global variables hanno soprattutto senso sul solo `update time`. + +Questo può diventare ambiguo quando si usano: + +- `wf_cnd_var_age_match` +- sorting per creation/update +- UI manage/inspection + +### Evidenze + +- `WiredConditionVariableAgeMatch.java` + - il target room/global vive soprattutto come valore di update +- le scelte di prodotto già fatte in `:wired` vanno in questa direzione + +### Impatto pratico + +- il builder può pensare che “tempo di creazione” sulle global sia forte quanto sulle user/furni +- condition o sort possono essere semanticamente strani anche se “funzionano” + +### Fix suggerito + +- trattare esplicitamente `room/global` come `updated-only` +- disabilitare in UI le opzioni che non hanno senso forte +- o documentare in modo molto chiaro la differenza + +--- + +## 4. Backlog Consigliato + +Ordine suggerito di intervento: + +1. **Chiudere il target `context`** + - o implementarlo davvero + - o toglierlo da UI/save/runtime +2. **Unificare il runtime** + - lasciare un solo entrypoint ufficiale +3. **Stabilire la regola sull’ordine effect** + - default stabile o ordine esplicito con extra +4. **Chiudere il leak dei movement update legacy** + - niente composer fuori collector quando wired moderno è attivo +5. **Ripensare il realtime di `:wired`** + - spostare il più possibile da polling a push + +--- + +## 5. Nota Finale + +Il sistema wired attuale è già molto più potente del modello classico, soprattutto per: + +- variabili +- signal routing +- selectors avanzati +- monitor +- manage/inspection + +Proprio per questo, le zone fragili oggi non sono tanto i box semplici, ma: + +- la coesistenza di due runtime +- la semantica temporale +- i movement stack +- le feature variabili ancora “mezze esposte” + +Questi sono i punti che più probabilmente spiegano i bug strani o intermittenti. diff --git a/docs/wired_full_reference.html b/docs/wired_full_reference.html new file mode 100644 index 00000000..6c15fcd3 --- /dev/null +++ b/docs/wired_full_reference.html @@ -0,0 +1,500 @@ + + + + + + Riferimento Completo Wired + + + + + + +
+
+
+
+
+ + Documentazione tecnica Wired +
+

+ Riferimento Completo Wired +

+

+ Questa pagina renderizza wired_full_reference.md in una vista HTML consultabile, + con struttura e interfaccia in italiano. Gli identificatori tecnici dei wired, delle classi e + delle chiavi restano invariati per mantenere la documentazione fedele al runtime. +

+
+
+
+
Sorgente
+
Markdown vivo
+
La pagina legge il file `.md` locale.
+
+
+
Lingua
+
Interfaccia italiana
+
Struttura e metadati localizzati.
+
+
+
Stile
+
Tailwind CDN
+
Layout leggibile, sticky nav e indice.
+
+
+
+
+ +
+ + +
+
+
+
+

Panoramica

+

+ Trovi all’inizio le regole del motore wired, poi il catalogo completo di trigger, effect, + selector, condition, extra e variabili. +

+
+
+ Engine + Tick + 154 wired catalogati + HTML da Markdown +
+
+
+ +
+
+ Caricamento della reference in corso... +
+ + +
+
+
+
+ + + + diff --git a/docs/wired_full_reference.md b/docs/wired_full_reference.md new file mode 100644 index 00000000..d27d60cf --- /dev/null +++ b/docs/wired_full_reference.md @@ -0,0 +1,1429 @@ +# Wired Full Reference + +## 1. Scope + +This document is a code-based reference for the current wired runtime in `Arcturus-Morningstar-Extended`. + +It covers: + +- general wired engine rules +- tick and delay rules +- protection and monitor rules +- custom variable rules +- every registered wired trigger, effect, selector, condition, extra, and variable definition + +Primary runtime sources used for this reference: + +- `Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java` +- `Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredManager.java` +- `Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java` +- `Emulator/src/main/java/com/eu/habbo/habbohotel/wired/tick/WiredTickService.java` +- `Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java` + +This file is meant to describe the runtime behavior and configuration surface, not the Nitro UI layout in detail. For `:wired` monitor and inspection tooling, also see `Arcturus-Morningstar-Extended/docs/wired_tools_reference.md`. + +--- + +## 2. Wired Engine, Tick, and General Runtime Rules + +### 2.1 Main runtime architecture + +The modern wired runtime is centered around these components: + +- `WiredManager` + - initializes the wired runtime + - loads config + - owns the centralized engine and stack index + - exposes high-level trigger methods such as user click, walk on, say, signal, game events, and so on +- `WiredEngine` + - receives `WiredEvent` objects + - finds candidate stacks through the room stack index + - evaluates selectors, conditions, extras, and effects + - enforces abuse protection, delayed queue limits, recursion limits, and diagnostics +- `RoomWiredStackIndex` + - caches stack membership so the engine can quickly find candidate stacks for a given event type +- `WiredTickService` + - runs a single global tick loop + - keeps repeaters and other tickables synchronized across rooms +- `WiredHandler` + - legacy compatibility path that still exists in the codebase + - still useful to understand older stack execution logic and some compatibility behavior + +### 2.2 Current execution model + +At a high level, the engine processes a stack like this: + +1. receive an event +2. find candidate stacks for the event type +3. check whether the trigger matches +4. build a `WiredContext` +5. run selectors first, so the target set is available +6. apply selection-filter extras if present +7. evaluate conditions +8. apply stack-level gates such as execution limit +9. activate the trigger and extras +10. execute or schedule effects + +Important consequences of this model: + +- selectors run before conditions in the new engine +- conditions can inspect the selected targets or even the whole stack +- effects may run immediately or with delay +- extras can modify selection, condition evaluation, effect ordering, and effect subset choice + +### 2.3 Tick rules + +The centralized tick service is defined in `WiredTickService`. + +Current core rules: + +- default global tick interval: `50ms` +- hard allowed range: `10ms` to `500ms` +- repeaters and other tickables use a shared global tick counter +- tickables are registered per room +- when a room is unloaded, tickables should be unregistered + +This means: + +- repeaters are synchronized with each other +- two repeaters with the same timing do not drift independently per room +- unload/cleanup behavior matters for room-scoped temporary state + +### 2.4 Delay rules + +The classic wired delay value is stored in half-second steps. + +Runtime rule: + +- `effective delay in milliseconds = delay * 500` + +Examples: + +- delay `0` = immediate execution +- delay `1` = `500ms` +- delay `2` = `1000ms` + +This rule is used both by the legacy path and by the new engine. + +### 2.5 Stack ordering rules + +There are several separate notions of order: + +- **stack candidate order** + - candidate stacks are found through the index for the specific event type +- **item ordering inside one tile stack** + - `WiredExecutionOrderUtil` sorts by: + - `z` + - then `item id` +- **effect subset modifiers** + - `wf_xtra_random` can choose only part of the effects + - `wf_xtra_unseen` can rotate through effects without repeats +- **ordered execution** + - `wf_xtra_exec_in_order` is the explicit “run in stable stack order” modifier + +Practical takeaway: + +- if there is no order modifier, execution may depend on the collection/order produced by the runtime path +- if exact order matters, `wf_xtra_exec_in_order` is the intended box to use + +### 2.6 Selection rules + +Selectors build or refine the `WiredTargets` inside `WiredContext`. + +In practice: + +- target users and target furni are built before conditions are checked +- later effects consume those selected targets +- some selectors are pure “build a selection” tools +- some extras then trim, sort, or invert that selection + +### 2.7 Condition rules + +Conditions are evaluated after selectors. + +General behavior: + +- if there are no conditions, the stack can continue directly to effects +- if there are conditions, all configured condition logic must pass according to the current evaluation mode +- `wf_xtra_or_eval` changes how the condition results are aggregated + +The runtime supports both: + +- ordinary condition matching +- grouped OR semantics through condition operators and the OR-eval extra + +### 2.8 Protection rules + +The wired runtime has multiple safety layers: + +- maximum steps per stack +- recursion depth protection +- per-room event rate limiting +- room temporary wired ban after abuse +- delayed queue cap +- execution budget / usage cap per room window + +Main defaults from runtime/config: + +- `wired.engine.maxStepsPerStack = 100` +- `wired.abuse.max.recursion.depth = 10` +- `wired.abuse.max.events.per.window = 100` +- `wired.abuse.rate.limit.window.ms = 10000` +- `wired.abuse.ban.duration.ms = 600000` +- `wired.monitor.usage.window.ms = 1000` +- `wired.monitor.usage.limit = 1000` +- `wired.monitor.delayed.events.limit = 100` + +### 2.9 Monitor and diagnostics rules + +The new engine tracks room diagnostics through `WiredRoomDiagnostics`. + +This is where `:wired` monitor gets values such as: + +- usage in current window +- delayed events count +- average execution time +- peak execution time +- recursion state +- heavy room status +- overload windows + +Heavy/overload decisions are based on rolling windows, not on a single event. + +### 2.10 Legacy compatibility notes + +The project still contains `WiredHandler`. + +Important practical notes: + +- `WiredManager` is the intended modern entrypoint +- `wired.engine.enabled` and `wired.engine.exclusive` are treated as compatibility-only flags by `WiredManager` +- `WiredHandler` still exists and is useful for compatibility and for understanding legacy behavior + +So when documenting stacks, it is best to think in terms of: + +- modern runtime: `WiredManager` + `WiredEngine` +- legacy compatibility surface: `WiredHandler` + +### 2.11 Custom variable rules + +Custom wired variables are defined by: + +- `wf_var_user` +- `wf_var_furni` +- `wf_var_room` + +Shared rules: + +- variable names must be unique across the whole room, even across different variable types +- allowed name length: `1..40` +- allowed characters: letters, numbers, `_` + +Availability rules: + +- `wf_var_user` + - room-scoped while the user is in the room + - or permanent +- `wf_var_furni` + - room-active while the room is active/loaded + - or permanent +- `wf_var_room` + - room-active + - or permanent + +Timestamp rules: + +- user variables: creation/update are tied to the assignment on that user +- furni variables: creation/update are tied to the assignment on that furni +- room variables: practically meaningful timestamp is mainly the last update time + +Current context-status note: + +- `context` appears in several variable-related layouts +- it is still partial / placeholder in several runtime paths +- `user`, `furni`, and `room/global` are the truly active targets today + +### 2.12 Useful global config keys + +| Key | Meaning | +|---|---| +| `wired.engine.enabled` | Compatibility-only legacy flag | +| `wired.engine.exclusive` | Compatibility-only legacy flag | +| `wired.engine.maxStepsPerStack` | Loop/step protection limit | +| `wired.engine.debug` | Verbose engine logging | +| `wired.custom.enabled` | Legacy custom wired compatibility behavior | +| `hotel.wired.furni.selection.count` | Max furni selection size stored by wired boxes | +| `hotel.wired.max_delay` | Max accepted delay value | +| `hotel.wired.message.max_length` | Max wired/bot text size | +| `wired.effect.teleport.delay` | Teleport effect delay | +| `wired.tick.interval.ms` | Global tick loop interval | +| `wired.tick.debug` | Tick debug logging | +| `wired.tick.thread.priority` | Tick thread priority | +| `wired.abuse.max.recursion.depth` | Recursion protection | +| `wired.abuse.max.events.per.window` | Event spam protection | +| `wired.abuse.rate.limit.window.ms` | Abuse window size | +| `wired.abuse.ban.duration.ms` | Temporary room wired-ban duration | +| `wired.monitor.usage.window.ms` | Usage monitor window size | +| `wired.monitor.usage.limit` | Execution budget per window | +| `wired.monitor.delayed.events.limit` | Delayed queue ceiling | +| `wired.monitor.overload.average.ms` | Overload average threshold | +| `wired.monitor.overload.peak.ms` | Overload peak threshold | +| `wired.monitor.heavy.usage.percent` | Heavy-room usage threshold | +| `wired.highscores.displaycount` | Wired highscore rows shown to users | + +--- + +## 3. Triggers + +### `wf_trg_walks_on_furni` + +- **Class:** `WiredTriggerHabboWalkOnFurni` +- **Behavior:** fires when a user walks onto the selected furni/tile stack. +- **Main settings:** selected furni, standard trigger cooldown. +- **Notes:** commonly used as the first event in movement or pressure-style stacks. + +### `wf_trg_walks_off_furni` + +- **Class:** `WiredTriggerHabboWalkOffFurni` +- **Behavior:** fires when a user leaves the selected furni/tile stack. +- **Main settings:** selected furni, standard trigger cooldown. +- **Notes:** useful for exit logic, cleanup logic, and delayed “leave area” patterns. + +### `wf_trg_click_furni` + +- **Class:** `WiredTriggerHabboClicksFurni` +- **Behavior:** fires when a user clicks a furni. +- **Main settings:** selected furni. +- **Notes:** click-based stacks often combine this with selectors or trigger-user conditions. + +### `wf_trg_click_tile` + +- **Class:** `WiredTriggerHabboClicksTile` +- **Behavior:** fires when a user clicks a tile. +- **Main settings:** selected click-tile furni / trigger area depending on setup. +- **Notes:** often used for invisible tile-style interactions. + +### `wf_trg_click_user` + +- **Class:** `WiredTriggerHabboClicksUser` +- **Behavior:** fires when one avatar clicks another avatar. +- **Main settings:** runtime flags such as menu blocking and rotation behavior. +- **Notes:** the event carries both the clicking user and the clicked user. + +### `wf_trg_user_performs_action` + +- **Class:** `WiredTriggerHabboPerformsAction` +- **Behavior:** fires when a user performs a configured avatar action. +- **Main settings:** action id and action parameter. +- **Notes:** pairs naturally with the matching positive/negative action conditions. + +### `wf_trg_enter_room` + +- **Class:** `WiredTriggerHabboEntersRoom` +- **Behavior:** fires when a user enters the room. +- **Main settings:** none beyond default cooldown. +- **Notes:** common for welcome logic, spawn logic, variable assignment, and snapshot restore. + +### `wf_trg_leave_room` + +- **Class:** `WiredTriggerHabboLeavesRoom` +- **Behavior:** fires when a user leaves the room. +- **Main settings:** none beyond default cooldown. +- **Notes:** common for cleanup and last-known-state stacks. + +### `wf_trg_says_something` + +- **Class:** `WiredTriggerHabboSaysKeyword` +- **Behavior:** fires when a user says the configured text/keyword. +- **Main settings:** text/keyword, message hiding mode. +- **Notes:** can optionally suppress the visible chat output when configured to hide the message. + +### `wf_trg_clock_counter` + +- **Class:** `WiredTriggerClockCounter` +- **Behavior:** fires when a selected counter reaches its configured match point. +- **Main settings:** target counter(s), counter matching behavior. +- **Notes:** often combined with `wf_act_control_clock` and `wf_act_adjust_clock`. + +### `wf_trg_periodically` + +- **Class:** `WiredTriggerRepeater` +- **Behavior:** fires on a repeating interval. +- **Main settings:** repeat interval. +- **Notes:** synchronized through the global tick service. + +### `wf_trg_period_short` + +- **Class:** `WiredTriggerRepeaterShort` +- **Behavior:** faster repeating trigger with short cadence. +- **Main settings:** short repeater timing. +- **Notes:** aligned to the global `50ms` tick service. + +### `wf_trg_period_long` + +- **Class:** `WiredTriggerRepeaterLong` +- **Behavior:** repeating trigger with longer cadence. +- **Main settings:** long repeater timing. +- **Notes:** intended for lower-frequency repeating behavior. + +### `wf_trg_state_changed` + +- **Class:** `WiredTriggerFurniStateToggled` +- **Behavior:** fires when the state of the selected furni changes. +- **Main settings:** selected furni. +- **Notes:** runtime is shared with `wf_trg_stuff_state`. + +### `wf_trg_stuff_state` + +- **Class:** `WiredTriggerFurniStateToggled` +- **Behavior:** same runtime behavior as `wf_trg_state_changed`. +- **Main settings:** selected furni. +- **Notes:** kept as a second key/alias for compatibility/content mapping. + +### `wf_trg_at_given_time` + +- **Class:** `WiredTriggerAtSetTime` +- **Behavior:** fires once after the configured time target is reached. +- **Main settings:** time value. +- **Notes:** behaves like a one-shot timer rather than a repeater. + +### `wf_trg_at_time_long` + +- **Class:** `WiredTriggerAtTimeLong` +- **Behavior:** long-duration variant of the set-time trigger. +- **Main settings:** time value. +- **Notes:** used when the short version is not sufficient for the desired range. + +### `wf_trg_collision` + +- **Class:** `WiredTriggerCollision` +- **Behavior:** fires when the configured collision case is detected. +- **Main settings:** collision participants / collision mode. +- **Notes:** can easily produce loops when combined with chase/flee unless protections are configured. + +### `wf_trg_game_starts` + +- **Class:** `WiredTriggerGameStarts` +- **Behavior:** fires when the room game starts. +- **Main settings:** none beyond default cooldown. +- **Notes:** useful for score resets, timers, and spawn setup. + +### `wf_trg_game_ends` + +- **Class:** `WiredTriggerGameEnds` +- **Behavior:** fires when the room game ends. +- **Main settings:** none beyond default cooldown. +- **Notes:** useful for rewards, cleanup, and reset logic. + +### `wf_trg_bot_reached_stf` + +- **Class:** `WiredTriggerBotReachedFurni` +- **Behavior:** fires when a bot reaches the selected furni. +- **Main settings:** bot path target furni. +- **Notes:** typically paired with bot movement effects. + +### `wf_trg_bot_reached_avtr` + +- **Class:** `WiredTriggerBotReachedHabbo` +- **Behavior:** fires when a bot reaches an avatar. +- **Main settings:** target avatar/source mode. +- **Notes:** useful for escort, interaction, or story-style flows. + +### `wf_trg_score_achieved` + +- **Class:** `WiredTriggerScoreAchieved` +- **Behavior:** fires when the configured score threshold is reached. +- **Main settings:** score threshold. +- **Notes:** usually tied to game or team score flows. + +### `wf_trg_game_team_win` + +- **Class:** `WiredTriggerTeamWins` +- **Behavior:** fires when a team wins the current room game. +- **Main settings:** target team. +- **Notes:** can be used for reward or celebration logic. + +### `wf_trg_game_team_lose` + +- **Class:** `WiredTriggerTeamLoses` +- **Behavior:** fires when a team loses the current room game. +- **Main settings:** target team. +- **Notes:** often paired with reset or consolation logic. + +### `wf_trg_recv_signal` + +- **Class:** `WiredTriggerReceiveSignal` +- **Behavior:** fires when a matching signal is received from `wf_act_send_signal`. +- **Main settings:** selected antenna(s), signal/channel matching. +- **Notes:** can receive user/furni payload carried by the signal event. + +--- + +## 4. Effects + +### `wf_act_toggle_state` + +- **Class:** `WiredEffectToggleFurni` +- **Behavior:** toggles the state of the selected furni. +- **Main settings:** selected furni, effect delay. +- **Notes:** one of the most common state-manipulation effects. + +### `wf_act_reset_timers` + +- **Class:** `WiredEffectResetTimers` +- **Behavior:** resets compatible timer/repeater-style boxes. +- **Main settings:** selected timer/counter/repeater items. +- **Notes:** used to restart timing flows cleanly. + +### `wf_act_match_to_sshot` + +- **Class:** `WiredEffectMatchFurni` +- **Behavior:** restores furni to a saved snapshot of state/position/rotation settings. +- **Main settings:** selected furni, snapshot match mode/settings. +- **Notes:** usually paired with move/rotate or state-change effects. + +### `wf_act_move_rotate` + +- **Class:** `WiredEffectMoveRotateFurni` +- **Behavior:** moves and/or rotates furni according to the configured pattern. +- **Main settings:** selected furni, movement direction, rotation behavior, effect delay. +- **Notes:** obeys move physics extras when present. + +### `wf_act_give_score` + +- **Class:** `WiredEffectGiveScore` +- **Behavior:** gives score to the target user/player. +- **Main settings:** score amount. +- **Notes:** room/game scoring effect. + +### `wf_act_show_message` + +- **Class:** `WiredEffectWhisper` +- **Behavior:** sends the configured message text. +- **Main settings:** message text, effect delay. +- **Notes:** text length is limited by wired message config. + +### `wf_act_teleport_to` + +- **Class:** `WiredEffectTeleport` +- **Behavior:** teleports the target user to the configured destination. +- **Main settings:** target furni/tile, effect delay. +- **Notes:** also respects `wired.effect.teleport.delay`. + +### `wf_act_join_team` + +- **Class:** `WiredEffectJoinTeam` +- **Behavior:** moves the target user into the selected team. +- **Main settings:** team id/color. +- **Notes:** game-specific utility effect. + +### `wf_act_leave_team` + +- **Class:** `WiredEffectLeaveTeam` +- **Behavior:** removes the target user from their team. +- **Main settings:** effect delay. +- **Notes:** typically used in game cleanup. + +### `wf_act_chase` + +- **Class:** `WiredEffectMoveFurniTowards` +- **Behavior:** moves furni toward the configured target. +- **Main settings:** selected furni, target source, movement distance/direction rules. +- **Notes:** can interact strongly with collision and movement validation. + +### `wf_act_flee` + +- **Class:** `WiredEffectMoveFurniAway` +- **Behavior:** moves furni away from the configured target. +- **Main settings:** selected furni, target source, movement rules. +- **Notes:** often paired with collision or proximity triggers. + +### `wf_act_move_to_dir` + +- **Class:** `WiredEffectChangeFurniDirection` +- **Behavior:** changes furni direction/rotation. +- **Main settings:** selected furni, new direction or direction mode. +- **Notes:** pure direction-change effect without full movement pathing. + +### `wf_act_give_score_tm` + +- **Class:** `WiredEffectGiveScoreToTeam` +- **Behavior:** gives score directly to a team. +- **Main settings:** team id and score amount. +- **Notes:** separate from single-user score. + +### `wf_act_toggle_to_rnd` + +- **Class:** `WiredEffectToggleRandom` +- **Behavior:** toggles a random compatible furni among the selected set. +- **Main settings:** selected furni. +- **Notes:** randomness is per execution. + +### `wf_act_move_furni_to` + +- **Class:** `WiredEffectMoveFurniTo` +- **Behavior:** moves furni to a configured target position. +- **Main settings:** selected furni, destination tile/furni, effect delay. +- **Notes:** works with movement physics and animation extras. + +### `wf_act_give_reward` + +- **Class:** `WiredEffectGiveReward` +- **Behavior:** gives a configured reward. +- **Main settings:** reward type, reward content, amount, inventory/catalog parameters depending on reward mode. +- **Notes:** may generate inventory items, badges, or related reward outputs depending on configuration. + +### `wf_act_call_stacks` + +- **Class:** `WiredEffectTriggerStacks` +- **Behavior:** triggers other stacks indirectly. +- **Main settings:** selected furni/tile sources. +- **Notes:** recursion protection is important here. + +### `wf_act_kick_user` + +- **Class:** `WiredEffectKickHabbo` +- **Behavior:** kicks the target user from the room. +- **Main settings:** target source, effect delay. +- **Notes:** administrative/gameplay removal effect. + +### `wf_act_mute_triggerer` + +- **Class:** `WiredEffectMuteHabbo` +- **Behavior:** mutes the target user. +- **Main settings:** mute duration / target source depending on layout. +- **Notes:** often used in moderation or mini-game penalties. + +### `wf_act_bot_teleport` + +- **Class:** `WiredEffectBotTeleport` +- **Behavior:** teleports the selected bot. +- **Main settings:** bot source and destination. +- **Notes:** bot-only effect. + +### `wf_act_bot_move` + +- **Class:** `WiredEffectBotWalkToFurni` +- **Behavior:** makes a bot walk toward the selected furni. +- **Main settings:** bot source, target furni. +- **Notes:** commonly paired with bot reached triggers. + +### `wf_act_bot_talk` + +- **Class:** `WiredEffectBotTalk` +- **Behavior:** makes a bot say configured text. +- **Main settings:** bot source, message text. +- **Notes:** subject to wired/bot text size limits. + +### `wf_act_bot_give_handitem` + +- **Class:** `WiredEffectBotGiveHandItem` +- **Behavior:** gives a handitem to a bot. +- **Main settings:** bot source, handitem id. +- **Notes:** bot cosmetic / state effect. + +### `wf_act_bot_follow_avatar` + +- **Class:** `WiredEffectBotFollowHabbo` +- **Behavior:** makes a bot follow an avatar. +- **Main settings:** bot source, avatar source. +- **Notes:** useful for escort or scripted behaviors. + +### `wf_act_bot_clothes` + +- **Class:** `WiredEffectBotClothes` +- **Behavior:** changes a bot’s clothes/look. +- **Main settings:** bot source, look string. +- **Notes:** bot appearance effect. + +### `wf_act_bot_talk_to_avatar` + +- **Class:** `WiredEffectBotTalkToHabbo` +- **Behavior:** makes a bot talk toward an avatar/target. +- **Main settings:** bot source, avatar target, text. +- **Notes:** dialogue-oriented bot effect. + +### `wf_act_give_respect` + +- **Class:** `WiredEffectGiveRespect` +- **Behavior:** gives respect to the target user. +- **Main settings:** respect amount / target source. +- **Notes:** social reward effect. + +### `wf_act_alert` + +- **Class:** `WiredEffectAlert` +- **Behavior:** sends an alert window/message. +- **Main settings:** alert text. +- **Notes:** distinct from whisper-style chat output. + +### `wf_act_give_handitem` + +- **Class:** `WiredEffectGiveHandItem` +- **Behavior:** gives a handitem to the target user. +- **Main settings:** handitem id. +- **Notes:** user state/cosmetic effect. + +### `wf_act_give_effect` + +- **Class:** `WiredEffectGiveEffect` +- **Behavior:** gives an avatar effect to the target user. +- **Main settings:** effect id. +- **Notes:** visual avatar effect. + +### `wf_act_freeze` + +- **Class:** `WiredEffectFreeze` +- **Behavior:** freezes the selected user targets. +- **Main settings:** target source, effect delay. +- **Notes:** mainly game/control utility. + +### `wf_act_unfreeze` + +- **Class:** `WiredEffectUnfreeze` +- **Behavior:** unfreezes the selected user targets. +- **Main settings:** target source, effect delay. +- **Notes:** counterpart to `wf_act_freeze`. + +### `wf_act_furni_to_user` + +- **Class:** `WiredEffectFurniToUser` +- **Behavior:** moves furni toward/on a user target. +- **Main settings:** furni source, user source, effect delay. +- **Notes:** movement batching/physics extras may change the visible result. + +### `wf_act_user_to_furni` + +- **Class:** `WiredEffectUserToFurni` +- **Behavior:** moves a user toward a furni target. +- **Main settings:** user source, furni target. +- **Notes:** a user-targeted movement effect. + +### `wf_act_furni_to_furni` + +- **Class:** `WiredEffectFurniToFurni` +- **Behavior:** moves one furni set onto another furni set. +- **Main settings:** primary furni source, secondary furni source. +- **Notes:** supports double-selection source flow. + +### `wf_act_set_altitude` + +- **Class:** `WiredEffectSetAltitude` +- **Behavior:** sets furni altitude. +- **Main settings:** selected furni, altitude value or altitude mode. +- **Notes:** used in advanced movement / stacking setups. + +### `wf_act_rel_mov` + +- **Class:** `WiredEffectRelativeMove` +- **Behavior:** moves furni using relative X/Y offsets. +- **Main settings:** selected furni, X offset, Y offset. +- **Notes:** easier to reason about than absolute destination when building movement loops. + +### `wf_act_control_clock` + +- **Class:** `WiredEffectControlClock` +- **Behavior:** controls counter boxes. +- **Main settings:** selected counter(s), action mode such as start/stop/reset/pause/resume. +- **Notes:** works directly with counter-based trigger/condition flows. + +### `wf_act_adjust_clock` + +- **Class:** `WiredEffectAdjustClock` +- **Behavior:** adjusts a counter’s current value. +- **Main settings:** selected counter(s), operation mode, amount. +- **Notes:** intended for dynamic counter manipulation. + +### `wf_act_move_rotate_user` + +- **Class:** `WiredEffectMoveRotateUser` +- **Behavior:** moves and/or rotates user targets. +- **Main settings:** user source, movement mode, direction/rotation settings. +- **Notes:** user-side analogue of furni move/rotate logic. + +### `wf_act_send_signal` + +- **Class:** `WiredEffectSendSignal` +- **Behavior:** sends a signal through antenna-based wiring. +- **Main settings:** selected antenna furni, signal payload/source options. +- **Notes:** can carry user/furni payload to `wf_trg_recv_signal`. + +### `wf_act_give_var` + +- **Class:** `WiredEffectGiveVariable` +- **Behavior:** assigns a custom variable to a compatible target. +- **Main settings:** variable definition, target type/source, overwrite flag, initial value if the variable has value. +- **Notes:** works with `wf_var_user` and `wf_var_furni`; room/global variables are definition-driven and do not need this assigner. + +### `wf_act_remove_var` + +- **Class:** `WiredEffectRemoveVariable` +- **Behavior:** removes a custom variable assignment from the selected target. +- **Main settings:** variable definition, target type/source. +- **Notes:** counterpart to `wf_act_give_var`. + +### `wf_act_change_var_val` + +- **Class:** `WiredEffectChangeVariableValue` +- **Behavior:** changes the value of a variable by applying an operation. +- **Main settings:** variable selection, operation, reference mode, constant or reference variable, reference source, target source. +- **Supported operations:** assign, add, subtract, multiply, divide, power, modulo, min, max, random, absolute, bitwise AND/OR/XOR/NOT, left shift, right shift. +- **Notes:** one of the most flexible variable effects; textual rendering is separate and handled by extras. + +--- + +## 5. Selectors + +### General selector notes + +Selectors typically do one or both of these: + +- build a new target set +- filter/transform an existing target set + +When the UI exposes classic selector options, those usually include: + +- filter the existing selection +- invert the result + +### `wf_slc_furni_area` + +- **Class:** `WiredEffectFurniArea` +- **Behavior:** selects furni in a configured area. +- **Main settings:** area size/position. +- **Notes:** foundational room-space selector. + +### `wf_slc_furni_neighborhood` + +- **Class:** `WiredEffectFurniNeighborhood` +- **Behavior:** selects furni in a local neighborhood around the source point. +- **Main settings:** neighborhood/radius. +- **Notes:** useful for adjacency-based logic. + +### `wf_slc_furni_bytype` + +- **Class:** `WiredEffectFurniByType` +- **Behavior:** selects furni by base furni type. +- **Main settings:** furni type. +- **Notes:** good for “all chairs”, “all switches”, and similar patterns. + +### `wf_slc_furni_altitude` + +- **Class:** `WiredEffectFurniAltitude` +- **Behavior:** selects furni by altitude relation/value. +- **Main settings:** compare mode and altitude target. +- **Notes:** useful in stacked build logic. + +### `wf_slc_furni_onfurni` + +- **Class:** `WiredEffectFurniOnFurni` +- **Behavior:** selects furni that are on top of other furni. +- **Main settings:** base furni selection. +- **Notes:** stack-inspection selector. + +### `wf_slc_furni_picks` + +- **Class:** `WiredEffectFurniPicks` +- **Behavior:** selects a hand-picked list of furni. +- **Main settings:** selected furni list. +- **Notes:** capped by `hotel.wired.furni.selection.count`. + +### `wf_slc_furni_signal` + +- **Class:** `WiredEffectFurniSignal` +- **Behavior:** selects furni carried by a signal event. +- **Main settings:** signal source mode. +- **Notes:** meaningful only in signal-driven stacks. + +### `wf_slc_users_area` + +- **Class:** `WiredEffectUsersArea` +- **Behavior:** selects users in a configured area. +- **Main settings:** area size/position. +- **Notes:** area equivalent of the furni selector. + +### `wf_slc_users_neighborhood` + +- **Class:** `WiredEffectUsersNeighborhood` +- **Behavior:** selects users in a nearby neighborhood. +- **Main settings:** neighborhood/radius. +- **Notes:** good for local interaction logic. + +### `wf_slc_users_signal` + +- **Class:** `WiredEffectUsersSignal` +- **Behavior:** selects users carried by a signal event. +- **Main settings:** signal source mode. +- **Notes:** signal-only context. + +### `wf_slc_users_bytype` + +- **Class:** `WiredEffectUsersByType` +- **Behavior:** selects users by runtime category. +- **Main settings:** user type such as habbo, bot, pet. +- **Notes:** useful for mixed rooms with bots and pets. + +### `wf_slc_users_team` + +- **Class:** `WiredEffectUsersTeam` +- **Behavior:** selects users by team membership. +- **Main settings:** team id/color. +- **Notes:** game-centric selector. + +### `wf_slc_users_byaction` + +- **Class:** `WiredEffectUsersByAction` +- **Behavior:** selects users by current action/state. +- **Main settings:** action type / action parameter. +- **Notes:** complements the action trigger/conditions. + +### `wf_slc_users_byname` + +- **Class:** `WiredEffectUsersByName` +- **Behavior:** selects users whose names are listed in the text area. +- **Main settings:** multiline list of usernames. +- **Notes:** direct name-driven selector. + +### `wf_slc_users_handitem` + +- **Class:** `WiredEffectUsersHandItem` +- **Behavior:** selects users holding a specific handitem. +- **Main settings:** handitem id. +- **Notes:** useful for role/item possession flows. + +### `wf_slc_users_onfurni` + +- **Class:** `WiredEffectUsersOnFurni` +- **Behavior:** selects users standing on selected furni. +- **Main settings:** base furni selection. +- **Notes:** common in pressure/tile gameplay. + +### `wf_slc_users_group` + +- **Class:** `WiredEffectUsersGroup` +- **Behavior:** selects users by group relationship in the room. +- **Main settings:** group relation/mode. +- **Notes:** useful for rights/group-room logic. + +### `wf_slc_furni_with_var` + +- **Class:** `WiredEffectFurniWithVariable` +- **Behavior:** selects furni that hold a chosen custom variable. +- **Main settings:** variable selection, optional value filter, comparison operator, constant or variable reference, reference source, selector options. +- **Notes:** if value filtering is disabled, it behaves as a presence-only selector. + +### `wf_slc_users_with_var` + +- **Class:** `WiredEffectUsersWithVariable` +- **Behavior:** selects users that hold a chosen custom variable. +- **Main settings:** variable selection, optional value filter, comparison operator, constant or variable reference, reference source, selector options. +- **Notes:** user-side analogue of the furni variable selector. + +--- + +## 6. Conditions + +### General condition notes + +Conditions can be thought of as gates for the stack. + +Common patterns: + +- positive/negative counterpart pairs +- threshold checks +- “match the current selection” +- variable-based checks +- time/date checks + +### `wf_cnd_has_furni_on` + +- **Class:** `WiredConditionFurniHaveFurni` +- **Behavior:** true if the configured furni have other furni on top. +- **Main settings:** target furni selection. + +### `wf_cnd_furnis_hv_avtrs` + +- **Class:** `WiredConditionFurniHaveHabbo` +- **Behavior:** true if the configured furni currently have avatars on top. +- **Main settings:** target furni selection. + +### `wf_cnd_stuff_is` + +- **Class:** `WiredConditionFurniTypeMatch` +- **Behavior:** true if the furni match the configured type. +- **Main settings:** furni type. + +### `wf_cnd_actor_in_group` + +- **Class:** `WiredConditionGroupMember` +- **Behavior:** true if the acting user is in the required group relation. +- **Main settings:** group relation. + +### `wf_cnd_user_count_in` + +- **Class:** `WiredConditionHabboCount` +- **Behavior:** true if room user count satisfies the configured threshold. +- **Main settings:** comparison and count value. + +### `wf_cnd_wearing_effect` + +- **Class:** `WiredConditionHabboHasEffect` +- **Behavior:** true if the target user is wearing the configured effect. +- **Main settings:** effect id. + +### `wf_cnd_wearing_badge` + +- **Class:** `WiredConditionHabboWearsBadge` +- **Behavior:** true if the target user wears the configured badge. +- **Main settings:** badge code. + +### `wf_cnd_time_less_than` + +- **Class:** `WiredConditionLessTimeElapsed` +- **Behavior:** true if less than the configured time has elapsed. +- **Main settings:** duration. + +### `wf_cnd_match_snapshot` + +- **Class:** `WiredConditionMatchStatePosition` +- **Behavior:** true if the current furni state/position matches the stored snapshot. +- **Main settings:** selected furni, snapshot fields to compare. + +### `wf_cnd_time_more_than` + +- **Class:** `WiredConditionMoreTimeElapsed` +- **Behavior:** true if more than the configured time has elapsed. +- **Main settings:** duration. + +### `wf_cnd_not_furni_on` + +- **Class:** `WiredConditionNotFurniHaveFurni` +- **Behavior:** logical negation of `wf_cnd_has_furni_on`. +- **Main settings:** target furni selection. + +### `wf_cnd_not_hv_avtrs` + +- **Class:** `WiredConditionNotFurniHaveHabbo` +- **Behavior:** logical negation of `wf_cnd_furnis_hv_avtrs`. +- **Main settings:** target furni selection. + +### `wf_cnd_not_stuff_is` + +- **Class:** `WiredConditionNotFurniTypeMatch` +- **Behavior:** logical negation of `wf_cnd_stuff_is`. +- **Main settings:** furni type. + +### `wf_cnd_not_user_count` + +- **Class:** `WiredConditionNotHabboCount` +- **Behavior:** logical negation of the user-count match. +- **Main settings:** comparison and count value. + +### `wf_cnd_not_wearing_fx` + +- **Class:** `WiredConditionNotHabboHasEffect` +- **Behavior:** true if the user is not wearing the configured effect. +- **Main settings:** effect id. + +### `wf_cnd_not_wearing_b` + +- **Class:** `WiredConditionNotHabboWearsBadge` +- **Behavior:** true if the user is not wearing the configured badge. +- **Main settings:** badge code. + +### `wf_cnd_not_in_group` + +- **Class:** `WiredConditionNotInGroup` +- **Behavior:** true if the user is not in the configured group relation. +- **Main settings:** group relation. + +### `wf_cnd_not_in_team` + +- **Class:** `WiredConditionNotInTeam` +- **Behavior:** true if the user is not in the configured team. +- **Main settings:** team id/color. + +### `wf_cnd_not_match_snap` + +- **Class:** `WiredConditionNotMatchStatePosition` +- **Behavior:** logical negation of snapshot match. +- **Main settings:** selected furni, snapshot fields to compare. + +### `wf_cnd_not_trggrer_on` + +- **Class:** `WiredConditionNotTriggerOnFurni` +- **Behavior:** true if the triggerer is not on the selected furni. +- **Main settings:** selected furni. + +### `wf_cnd_actor_in_team` + +- **Class:** `WiredConditionTeamMember` +- **Behavior:** true if the actor belongs to the required team. +- **Main settings:** team id/color. + +### `wf_cnd_trggrer_on_frn` + +- **Class:** `WiredConditionTriggerOnFurni` +- **Behavior:** true if the triggerer is on the selected furni. +- **Main settings:** selected furni. + +### `wf_cnd_has_handitem` + +- **Class:** `WiredConditionHabboHasHandItem` +- **Behavior:** true if the user currently holds the configured handitem. +- **Main settings:** handitem id. + +### `wf_cnd_not_has_handitem` + +- **Class:** `WiredConditionNotHabboHasHandItem` +- **Behavior:** logical negation of the handitem condition. +- **Main settings:** handitem id. + +### `wf_cnd_date_rng_active` + +- **Class:** `WiredConditionDateRangeActive` +- **Behavior:** true if current server time is between the configured absolute date/time bounds. +- **Main settings:** start timestamp, end timestamp. + +### `wf_cnd_valid_moves` + +- **Class:** `WiredConditionMovementValidation` +- **Behavior:** simulates movement-related effects in the current stack and fails if a movement would be invalid. +- **Main settings:** no major user-facing setting besides stack composition. +- **Notes:** especially useful before move/rotate stacks. + +### `wf_cnd_counter_time_matches` + +- **Class:** `WiredConditionCounterTimeMatches` +- **Behavior:** true if the selected counter(s) match the configured time value. +- **Main settings:** counter selection, compare mode, target value, quantifier. + +### `wf_cnd_match_time` + +- **Class:** `WiredConditionMatchTime` +- **Behavior:** true if server local time matches the configured clock rule. +- **Main settings:** hour/minute/second or related time fields. + +### `wf_cnd_match_date` + +- **Class:** `WiredConditionMatchDate` +- **Behavior:** true if server local date matches the configured date rule. +- **Main settings:** weekday/day/month/year. + +### `wf_cnd_actor_dir` + +- **Class:** `WiredConditionActorDir` +- **Behavior:** true if the actor faces the configured direction. +- **Main settings:** direction. + +### `wf_cnd_slc_quantity` + +- **Class:** `WiredConditionSelectionQuantity` +- **Behavior:** true if the current selection size matches the configured threshold. +- **Main settings:** compare mode and amount. + +### `wf_cnd_user_performs_action` + +- **Class:** `WiredConditionUserPerformsAction` +- **Behavior:** true if the tracked user action matches. +- **Main settings:** action id / action parameter. + +### `wf_cnd_not_user_performs_action` + +- **Class:** `WiredConditionNotUserPerformsAction` +- **Behavior:** logical negation of the user action condition. +- **Main settings:** action id / action parameter. + +### `wf_cnd_has_altitude` + +- **Class:** `WiredConditionHasAltitude` +- **Behavior:** true if the selected furni satisfy the altitude comparison. +- **Main settings:** compare mode and altitude value. + +### `wf_cnd_triggerer_match` + +- **Class:** `WiredConditionTriggererMatch` +- **Behavior:** true if the triggerer matches the required target/source rule. +- **Main settings:** target source/match mode. + +### `wf_cnd_not_triggerer_match` + +- **Class:** `WiredConditionNotTriggererMatch` +- **Behavior:** logical negation of triggerer match. +- **Main settings:** target source/match mode. + +### `wf_cnd_team_has_score` + +- **Class:** `WiredConditionTeamHasScore` +- **Behavior:** true if the selected team score satisfies the configured comparison. +- **Main settings:** team id, comparison mode, score threshold. + +### `wf_cnd_team_has_rank` + +- **Class:** `WiredConditionTeamHasRank` +- **Behavior:** true if the selected team currently has the configured rank/placement. +- **Main settings:** team id, rank target. + +### `wf_cnd_has_var` + +- **Class:** `WiredConditionHasVariable` +- **Behavior:** true if the target entity holds the chosen variable. +- **Main settings:** variable selection, quantifier (`all` / `any`), variable source target. +- **Notes:** current layout/runtime is centered on user and furni variables; context exists as future placeholder. + +### `wf_cnd_neg_has_var` + +- **Class:** `WiredConditionNotHasVariable` +- **Behavior:** logical negation of `wf_cnd_has_var`. +- **Main settings:** variable selection, quantifier, source target. + +### `wf_cnd_var_val_match` + +- **Class:** `WiredConditionVariableValueMatch` +- **Behavior:** compares a variable value against a constant or another variable. +- **Main settings:** variable selection, compare type (`>`, `≥`, `=`, `≤`, `<`, `≠`), reference mode, reference variable/source, quantifier. +- **Notes:** room/global variables are supported here; context remains partial. + +### `wf_cnd_var_age_match` + +- **Class:** `WiredConditionVariableAgeMatch` +- **Behavior:** compares variable age against a duration. +- **Main settings:** variable selection, compare field (`creation` or `update` time), compare type (`lower than` / `higher than`), duration value + unit, quantifier, source. +- **Notes:** room/global variables are mostly meaningful for update time. + +--- + +## 7. Extras + +### `wf_xtra_random` + +- **Class:** `WiredExtraRandom` +- **Behavior:** executes only a random subset of effects instead of all effects. +- **Main settings:** number of effects to choose, optional recent-history protection. +- **Notes:** effect subset changes at each execution. + +### `wf_xtra_unseen` + +- **Class:** `WiredExtraUnseen` +- **Behavior:** rotates through effects without repeating one until the full cycle is exhausted. +- **Main settings:** hidden runtime state / no-repeat cycle. +- **Notes:** useful when true round-robin behavior is preferred over randomness. + +### `wf_blob` + +- **Class:** `WiredBlob` +- **Behavior:** special wired/game helper item. +- **Main settings:** blob-specific gameplay/runtime state. +- **Notes:** not a normal logic extra in the same sense as the others, but it is registered in the wired extra family. + +### `wf_xtra_or_eval` + +- **Class:** `WiredExtraOrEval` +- **Behavior:** changes how condition results are aggregated. +- **Main settings:** evaluation mode and compare value. +- **Notes:** lets stacks use modes beyond plain “all conditions must pass”. + +### `wf_xtra_filter_furni` + +- **Class:** `WiredExtraFilterFurni` +- **Behavior:** trims the current furni selection to a limited quantity. +- **Main settings:** quantity. +- **Notes:** selection-filter extra, not a normal selector. + +### `wf_xtra_filter_user` + +- **Class:** `WiredExtraFilterUser` +- **Behavior:** trims the current user selection to a limited quantity. +- **Main settings:** quantity. +- **Notes:** same runtime family as `wf_xtra_filter_users`. + +### `wf_xtra_filter_users` + +- **Class:** `WiredExtraFilterUser` +- **Behavior:** same runtime behavior as `wf_xtra_filter_user`. +- **Main settings:** quantity. +- **Notes:** alias key kept for content compatibility. + +### `wf_xtra_filter_furni_by_var` + +- **Class:** `WiredExtraFilterFurniByVariable` +- **Behavior:** sorts furni by variable metric and keeps only the top N. +- **Main settings:** variable selection, sort mode, quantity mode, constant quantity or variable reference, reference source. +- **Supported sort modes:** highest value, lowest value, oldest creation, latest creation, oldest update, latest update. + +### `wf_xtra_filter_users_by_var` + +- **Class:** `WiredExtraFilterUsersByVariable` +- **Behavior:** sorts users by variable metric and keeps only the top N. +- **Main settings:** variable selection, sort mode, quantity mode, constant quantity or variable reference, reference source. +- **Supported sort modes:** highest value, lowest value, oldest creation, latest creation, oldest update, latest update. + +### `wf_xtra_mov_carry_users` + +- **Class:** `WiredExtraMoveCarryUsers` +- **Behavior:** carries users together with moved furni. +- **Main settings:** carry mode. +- **Notes:** affects how movement results are applied when furni move. + +### `wf_xtra_mov_no_animation` + +- **Class:** `WiredExtraMoveNoAnimation` +- **Behavior:** suppresses movement animation. +- **Main settings:** none besides presence in stack. +- **Notes:** intended for instant or hidden movement behavior. + +### `wf_xtra_anim_time` + +- **Class:** `WiredExtraAnimationTime` +- **Behavior:** overrides movement animation time. +- **Main settings:** animation duration. +- **Notes:** influences visual pacing, not core selection logic. + +### `wf_xtra_mov_physics` + +- **Class:** `WiredExtraMovePhysics` +- **Behavior:** changes the physics rules applied during movement. +- **Main settings:** physics flags such as collision/pass-through/stack behavior depending on layout. +- **Notes:** important for advanced furni movement setups. + +### `wf_xtra_exec_in_order` + +- **Class:** `WiredExtraExecuteInOrder` +- **Behavior:** forces ordered effect execution. +- **Main settings:** none besides presence in stack. +- **Notes:** the explicit “do not rely on arbitrary order” extra. + +### `wf_xtra_execution_limit` + +- **Class:** `WiredExtraExecutionLimit` +- **Behavior:** allows the stack to execute only a configured number of times per window. +- **Main settings:** max executions, time window. +- **Notes:** stack-level throttle. + +### `wf_xtra_text_output_username` + +- **Class:** `WiredExtraTextOutputUsername` +- **Behavior:** exposes one or more usernames as a text placeholder for other wired text. +- **Main settings:** placeholder name, placeholder type (single/multiple), delimiter, user source. +- **Notes:** works like a text injector for later wired text output. + +### `wf_xtra_text_output_furni_name` + +- **Class:** `WiredExtraTextOutputFurniName` +- **Behavior:** exposes furni names as a text placeholder. +- **Main settings:** placeholder name, placeholder type (single/multiple), delimiter, furni source. +- **Notes:** furni-name counterpart to username output. + +### `wf_xtra_text_output_variable` + +- **Class:** `WiredExtraTextOutputVariable` +- **Behavior:** exposes a variable value as a text placeholder. +- **Main settings:** placeholder name, variable selection, display type (`numeric` / `textual`), placeholder type (`single` / `multiple`), delimiter, dynamic variable source. +- **Notes:** textual display works only when the selected variable is connected through `wf_xtra_var_text_connector`. + +### `wf_xtra_var_text_connector` + +- **Class:** `WiredExtraVariableTextConnector` +- **Behavior:** maps numeric values to text labels for a variable. +- **Main settings:** text area mapping in the form `0=text`, `1=text`, and so on. +- **Notes:** must live in the same stack context as the corresponding `wf_var_*` definition to be meaningful. + +--- + +## 8. Variable Definitions + +### `wf_var_user` + +- **Class:** `WiredExtraUserVariable` +- **Behavior:** defines a custom variable that can be assigned to users. +- **Main settings:** variable name, `has value` flag, availability (`while user is in room` / `permanent`). +- **Notes:** assignment is done through `wf_act_give_var`; timestamps belong to the assignment on the individual user. + +### `wf_var_furni` + +- **Class:** `WiredExtraFurniVariable` +- **Behavior:** defines a custom variable that can be assigned to furni. +- **Main settings:** variable name, `has value` flag, availability (`while room is active` / `permanent`). +- **Notes:** non-permanent assignments are cleaned when the room is unloaded/unregistered from room tickables. + +### `wf_var_room` + +- **Class:** `WiredExtraRoomVariable` +- **Behavior:** defines a room/global variable. +- **Main settings:** variable name, availability (`while room is active` / `permanent`). +- **Notes:** always has a value; there is no separate “has value” checkbox for room variables. + +--- + +## 9. Special Wired Items + +These are part of the wired ecosystem, even if they are not regular trigger/effect/selector/condition/extra boxes. + +### `wf_highscore` + +- **Class:** `InteractionWiredHighscore` +- **Behavior:** wired highscore furniture that stores and displays ranked score data. +- **Main settings:** score type, clear/reset policy, display behavior depending on furniture configuration. +- **Notes:** governed also by `wired.highscores.displaycount`. + +--- + +## 10. Practical Design Notes + +### 10.1 If exact order matters + +Use: + +- `wf_xtra_exec_in_order` + +Do not rely on “it seems to run in that order” when the stack becomes more complex. + +### 10.2 If the stack performs movement + +Prefer to think about: + +- movement validation +- movement physics extras +- carry-users extra +- animation/no-animation extras +- snapshot restore effects + +Movement stacks are where most subtle runtime interactions appear. + +### 10.3 If the stack uses variables + +Remember: + +- variable name must be room-unique +- target type matters +- room/global variables are definition-driven +- textual rendering requires the text connector +- `context` is not yet fully implemented everywhere + +### 10.4 If the stack uses repeaters/timers + +Remember: + +- repeaters are synchronized on the global tick loop +- delay units are half-seconds +- counters, repeaters, and timer-style triggers often need explicit reset/control logic + +### 10.5 If the stack is heavy + +Check: + +- selection size +- number of delayed effects +- recursion or self-trigger chains +- random/unseen subsets +- execution limits +- room diagnostics in `:wired` + +--- + +## 11. Quick Alias / Shared Runtime Notes + +- `wf_trg_state_changed` and `wf_trg_stuff_state` share the same runtime. +- `wf_xtra_filter_user` and `wf_xtra_filter_users` share the same runtime. +- Several positive/negative conditions are simple logical counterparts. +- `wf_act_give_var`, `wf_act_remove_var`, `wf_act_change_var_val`, variable selectors, and variable conditions all operate on top of the same custom variable system defined by `wf_var_*`. diff --git a/docs/wired_send_signal_flow.html b/docs/wired_send_signal_flow.html new file mode 100644 index 00000000..79dd9f69 --- /dev/null +++ b/docs/wired_send_signal_flow.html @@ -0,0 +1,283 @@ + + + + + + Wired Send Signal - Flow Attuale + + + +
+
+ + Wired · Send Signal + +

Schema del flow attuale

+

+ Questa pagina descrive il comportamento attuale del wired send signal, con tutti i casi principali: + antenne, utenti, furni, conteggi e combinazioni finali. È pensata da inoltrare così com'è per un controllo del flow. +

+
+ +
+
+

Formula finale

+

antenne × utenti × furni

+

+ Il numero totale di segnali emessi è dato dal prodotto tra antenne valide, rami utente e rami furni. +

+
+
+

Regola utenti

+

+ Con per ogni utente = disattivo, il ramo usa sempre l'utente che innesca. + Con per ogni utente = attivo, il ramo usa l'utente che innesca più gli utenti trovati dalla source. +

+
+
+

Regola furni

+

+ Se la source furni restituisce elementi, il flow attuale apre un ramo per ogni furni. + Se non restituisce nulla, viene emesso un solo ramo senza furni allegati. +

+
+
+ +
+

Pseudo flow

+
    +
  1. 1. Vengono risolte le antenne destinazione.
  2. +
  3. 2. Restano valide solo le antenne reali; se non ne resta nessuna, il flow si ferma.
  4. +
  5. 3. Viene preso l'utente che ha innescato, se esiste.
  6. +
  7. 4. Vengono risolti gli utenti dalla source utenti.
  8. +
  9. 5. Vengono risolti i furni dalla source furni.
  10. +
  11. 6. Se “per ogni utente” è attivo, si costruisce una lista unica con: +
      +
    • sempre l'utente che innesca, se presente;
    • +
    • poi tutti gli utenti della source;
    • +
    • senza duplicati.
    • +
    +
  12. +
  13. 7. Se “per ogni utente” è disattivo, si usa un solo ramo utente: + l'utente che innesca.
  14. +
  15. 8. Se la source furni ha elementi, si apre un ramo per ogni furni.
  16. +
  17. 9. Se la source furni è vuota, si apre un solo ramo senza furni.
  18. +
  19. 10. Per ogni combinazione antenna + utente + furni viene emesso un segnale separato.
  20. +
  21. 11. Ogni segnale porta con sé: +
      +
    • la tile dell'antenna;
    • +
    • l'utente del ramo corrente, se presente;
    • +
    • l'utente originario che ha innescato la chain, se presente;
    • +
    • il furni del ramo corrente, se presente;
    • +
    • le context variables;
    • +
    • la profondità della chain aggiornata;
    • +
    • i conteggi utenti/furni esposti al ramo ricevente.
    • +
    +
  22. +
+
+ +
+
+

Tabella casi · Utenti

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Per ogni utenteUtente che innescaSource utentiRami utente emessiNota
DisattivoPresenteVuota1Parte sempre con l'utente che innesca.
DisattivoPresente3 utenti1Gli utenti source non diventano rami separati.
DisattivoAssenteVuota1Parte un ramo senza utente.
AttivoPresenteVuota1L'utente che innesca viene sempre incluso.
AttivoPresente3 utenti diversi4Utente che innesca + 3 utenti della source.
AttivoPresenteContiene già l'utente che innescaUtenti uniciNessun duplicato.
AttivoAssente3 utenti3Usa solo gli utenti trovati dalla source.
AttivoAssenteVuota1Parte un ramo senza utente.
+
+
+ +
+

Tabella casi · Furni

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Source furniFurni trovatiRami furni emessiDato nel singolo ramo
Vuota01Nessun furni allegato
1 furni11Quel furni
3 furni33Un furni diverso per ramo
7 furni77Un furni diverso per ramo
+
+

+ Nel comportamento attuale, se la source furni restituisce elementi, il flow si apre sempre per furni singolo. +

+
+
+ +
+

Tabella casi · Combinazioni complete

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CasoAntenneRami utenteRami furniTotale segnali
Utente che innesca presente, per ogni utente disattivo, 3 furni2136
Utente che innesca presente, per ogni utente attivo, source utenti con 3 utenti, 3 furni24324
Utente che innesca presente, selector utenti vuoto, 7 furni1177
Nessun utente, source utenti vuota, 7 furni1177
Nessuna antenna valida0qualsiasiqualsiasi0
+
+
+ +
+
+

Conteggi esposti al ricevente

+
    +
  • Conteggio utenti con “per ogni utente” attivo: numero di utenti unici del merge tra utente che innesca e source utenti.
  • +
  • Conteggio utenti con “per ogni utente” disattivo: se la source utenti ha elementi, vale il numero di utenti trovati dalla source; altrimenti vale 1 se esiste l'utente che innesca, altrimenti 0.
  • +
  • Conteggio furni: nel singolo ramo vale 1 se c'è un furni allegato, altrimenti 0.
  • +
+
+
+

Nota importante sul comportamento attuale

+

+ Oggi il flow reale fa fan-out per furni quando la source furni restituisce elementi. Quindi, se dalla source arrivano 7 furni, + il sistema apre 7 rami furni distinti. Questo è importante perché impatta sia il numero totale dei segnali sia i conteggi + osservati a valle. +

+

+ Inoltre il segnale conserva anche l'utente originario che ha avviato la chain, separato dall'utente del ramo corrente. +

+
+
+ + +
+ + diff --git a/docs/wired_tools_implementation_summary.md b/docs/wired_tools_implementation_summary.md new file mode 100644 index 00000000..7dbc5c28 --- /dev/null +++ b/docs/wired_tools_implementation_summary.md @@ -0,0 +1,283 @@ +# Wired Creator Tools Implementation Summary + +## 1. Purpose + +This document summarizes the `:wired` work completed in this development cycle. + +It is intended as a project-facing summary of: + +- what was added +- where it lives +- what is already working +- what is still intentionally left as future work + +--- + +## 2. Main goals completed + +The current `:wired` implementation now provides: + +1. a dedicated Nitro UI window +2. monitor and inspection tooling +3. room/user/furni/global variable views +4. inline editing for selected values +5. live wired diagnostics from the server +6. error/warning history with details +7. server-side diagnostics configuration through DB settings + +--- + +## 3. Nitro-V3 work + +Main file: + +- `Nitro-V3/src/components/wired-tools/WiredCreatorToolsView.tsx` + +### 3.1 UI window + +The `:wired` tool now has these main tabs: + +- `Monitor` +- `Variables` +- `Inspection` +- `Chests` +- `Settings` + +Current active work is mainly in: + +- `Monitor` +- `Inspection` + +`Chests` and `Settings` are currently placeholder/future-facing areas. + +### 3.2 Inspection + +Implemented: + +- element type switcher (`furni`, `user`, `global`) +- preview area +- variable table +- `Keep selected` +- inline editing + +#### Furni + +Added support for: + +- detailed furni variables +- live preview +- wall/floor-specific handling +- teleport metadata +- inline edits for state/position/rotation/altitude/wall offset + +#### User + +Added support for: + +- user/bot/pet identity +- rights / owner / group admin flags +- mute / trading / frozen flags +- team / sign / dance / idle / hand item / effect display +- room entry method and teleport entry id +- inline edits for position and direction + +#### Global + +Added support for: + +- room counts +- wired timer +- team scores and sizes +- room/group ids +- server/client timezone +- current server time breakdown + +### 3.3 Monitor + +Implemented: + +- live stats table +- log summary list +- full log history +- info/documentation popup +- error information popup + +The auxiliary monitor windows now use proper Nitro card windows, so they are: + +- draggable +- resizable +- closed through the normal Nitro close button + +--- + +## 4. Nitro_Render_V3 work + +Renderer-side work was focused on making Nitro receive enough metadata for the new UI. + +Main areas: + +- new wired monitor packet parsing +- room/session metadata extensions +- furni metadata extensions +- user metadata extensions + +### 4.1 Monitor data + +The renderer now parses and exposes: + +- usage budget values +- delayed queue values +- execution timing values +- heavy/overload thresholds +- current logs +- history rows + +### 4.2 Room metadata + +The renderer/session flow was extended to expose values used by Nitro: + +- room furni limit +- room group id +- hotel timezone / hotel time snapshot + +### 4.3 Furni metadata + +The furni info path now exposes values used by the inspector, including: + +- dimensions +- `items_base`-driven flags such as sit/lay/stand/stack +- teleport target metadata + +### 4.4 User metadata + +The user/unit data path now exposes values used by the inspector, including: + +- room entry method +- room entry teleport id +- identity data for user/bot/pet + +--- + +## 5. Emulator work + +Main areas: + +- wired diagnostics engine +- monitor request/response packet +- room/user/furni metadata support +- configuration migration to `wired_emulator_settings` + +### 5.1 Wired diagnostics + +Added server-side room diagnostics with: + +- usage budget tracking +- delayed event queue tracking +- average/peak execution timing +- overload detection +- heavy-room detection +- recursion protection logging +- killed-room protection logging + +### 5.2 Diagnostics logs + +Logs now carry: + +- type +- severity +- count +- reason +- source label +- source id +- history entries with occurrence timestamps + +### 5.3 Trigger/runtime fixes + +Important behaviour fixes added during this work: + +- empty repeater stacks no longer count as executable work +- monitor usage is consumed later in the execution path, closer to real execution +- timer/repeater behaviour is less noisy in diagnostics + +### 5.4 Monitor packet + +A dedicated request/response path was added so Nitro can poll live room diagnostics. + +### 5.5 Configuration migration + +All wired config is being moved out of `emulator_settings` and into: + +- `wired_emulator_settings` + +This now includes both: + +- existing wired runtime settings +- the new `:wired` monitor threshold settings + +Migration file: + +- `Database Updates/002_move_wired_settings_to_wired_emulator_settings.sql` + +--- + +## 6. What the monitor currently measures + +The monitor currently measures: + +- execution budget consumed in the current server window +- delayed events currently pending +- average execution time inside the current window +- peak execution time inside the current window +- recursion depth +- remaining killed-room cooldown +- room heavy state +- room furni counts +- renderer custom variable counts on room items + +--- + +## 7. What is configurable now + +Current DB-configurable areas include: + +- engine enable/debug/exclusive/max-steps +- custom wired compatibility mode +- furni selection limit +- max delay / max text length +- teleport delay +- tick interval/debug/priority +- abuse protection thresholds +- monitor usage/delayed/heavy/overload thresholds + +All of these are documented in: + +- `docs/wired_tools_reference.md` + +--- + +## 8. Known limitations / future work + +Current known limitations: + +- `Permanent furni vars` uses a fixed UI denominator (`60`) +- `@wired_timer` is still client-side time since room entry +- `Chests` and `Settings` are not fully implemented yet +- legacy wired configuration keys are still present for database compatibility, but runtime execution now goes only through the new engine + +Good future tasks: + +- make `Permanent furni vars` fully server-driven +- add export/copy actions for monitor history +- add more detailed filtering/search in history +- document chest/settings once implemented +- optionally remove the compatibility keys entirely once old database defaults are no longer needed + +--- + +## 9. Recommended rollout order + +1. run the wired settings migration SQL +2. restart the emulator +3. refresh renderer/client +4. verify monitor values in a real room +5. tune `wired.monitor.*` thresholds using the new DB table diff --git a/docs/wired_tools_reference.md b/docs/wired_tools_reference.md new file mode 100644 index 00000000..d5dbda23 --- /dev/null +++ b/docs/wired_tools_reference.md @@ -0,0 +1,554 @@ +# Wired Creator Tools (`:wired`) Reference + +## 1. Scope + +This document describes the current `:wired` tooling that was added across: + +- `Arcturus-Morningstar-Extended` (server-side data, diagnostics, config) +- `Nitro_Render_V3` (packet parsing and room/session metadata) +- `Nitro-V3` (UI, monitor, inspection, previews, inline editing) + +It focuses on: + +- the `Monitor` tab +- the `Inspection` tab (`furni`, `user`, `global`) +- the `wired_emulator_settings` database table +- the formulas and thresholds behind the monitor statistics + +--- + +## 2. High-level architecture + +### 2.1 Data flow + +`Emulator` -> `Nitro_Render_V3` -> `Nitro-V3` + +- The emulator computes room diagnostics and exposes extra room, furni, user, and monitor metadata. +- The renderer parses those packets and stores the values in room/session data objects. +- Nitro reads those values and renders them in the `:wired` UI. + +### 2.2 Main files + +- Emulator diagnostics: + - `Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredEngine.java` + - `Emulator/src/main/java/com/eu/habbo/habbohotel/wired/core/WiredRoomDiagnostics.java` + - `Emulator/src/main/java/com/eu/habbo/messages/outgoing/wired/WiredMonitorDataComposer.java` +- Renderer parsing: + - `Nitro_Render_V3/packages/communication/src/messages/parser/roomevents/WiredMonitorDataParser.ts` +- Nitro UI: + - `Nitro-V3/src/components/wired-tools/WiredCreatorToolsView.tsx` + +--- + +## 3. Database configuration + +## 3.1 New table + +Wired configuration is now separated from `emulator_settings` into: + +```sql +wired_emulator_settings ( + key, + value, + comment +) +``` + +Migration file: + +- `Database Updates/002_move_wired_settings_to_wired_emulator_settings.sql` + +The migration: + +1. creates `wired_emulator_settings` +2. imports existing wired values from `emulator_settings` +3. inserts defaults for new monitor/diagnostic keys when missing +4. removes migrated wired keys from `emulator_settings` + +### 3.2 Compatibility behaviour + +The emulator still has a **fallback read path**: + +- first it reads wired keys from `wired_emulator_settings` +- if a wired key is still missing there, it can still read it from the old `emulator_settings` + +This allows safe rollout during migration. + +## 3.3 Wired configuration keys + +| Key | Default | Purpose | +|---|---:|---| +| `wired.engine.enabled` | `1` | Compatibility flag kept for old configs. Wired now runs through the new engine only. | +| `wired.engine.exclusive` | `1` | Compatibility flag kept for old configs. Wired now runs through the new engine only. | +| `wired.engine.maxStepsPerStack` | `100` | Maximum internal processing steps allowed for one stack execution. | +| `wired.engine.debug` | `0` | Enables verbose wired engine logging. | +| `wired.custom.enabled` | `0` | Enables legacy custom wired compatibility logic. | +| `hotel.wired.furni.selection.count` | `5` | Maximum furni count selectable/storable by wired boxes. | +| `hotel.wired.max_delay` | `20` | Maximum accepted wired delay value for delayed effects. | +| `hotel.wired.message.max_length` | `100` | Maximum length of wired/bot text fields. | +| `wired.effect.teleport.delay` | `500` | Delay in milliseconds used by wired teleports. | +| `wired.place.under` | `0` | Allows wired furniture placement under other items. | +| `wired.tick.interval.ms` | `50` | Global tick interval in milliseconds for repeater-style wired. | +| `wired.tick.resolution` | `100` | Legacy compatibility tick resolution value. | +| `wired.tick.debug` | `0` | Enables verbose logging for the tick service. | +| `wired.tick.thread.priority` | `6` | Java thread priority for the tick service. | +| `wired.highscores.displaycount` | `25` | Maximum wired highscore entries shown to the user. | +| `wired.abuse.max.recursion.depth` | `10` | Maximum recursive wired depth before execution stops. | +| `wired.abuse.max.events.per.window` | `100` | Maximum identical events allowed inside the abuse rate-limit window. | +| `wired.abuse.rate.limit.window.ms` | `10000` | Time window in milliseconds used by the abuse limiter. | +| `wired.abuse.ban.duration.ms` | `600000` | Room wired-ban duration in milliseconds after abuse detection. | +| `wired.monitor.usage.window.ms` | `1000` | Rolling window size used to calculate monitor usage. | +| `wired.monitor.usage.limit` | `1000` | Maximum usage budget allowed in one monitor window. | +| `wired.monitor.delayed.events.limit` | `100` | Maximum delayed wired events that may be pending in one room. | +| `wired.monitor.overload.average.ms` | `50` | Average execution threshold in milliseconds for overload tracking. | +| `wired.monitor.overload.peak.ms` | `150` | Peak execution threshold in milliseconds for overload tracking. | +| `wired.monitor.overload.consecutive.windows` | `2` | Consecutive overloaded windows required before `EXECUTOR_OVERLOAD`. | +| `wired.monitor.heavy.usage.percent` | `70` | Usage percentage threshold that contributes to `MARKED_AS_HEAVY`. | +| `wired.monitor.heavy.consecutive.windows` | `5` | Consecutive heavy windows required before the room is marked heavy. | +| `wired.monitor.heavy.delayed.percent` | `60` | Delayed queue percentage threshold that contributes to the heavy state. | + +--- + +## 4. Monitor tab + +## 4.1 Statistics shown in the UI + +The `Monitor` tab currently shows: + +- `Wired usage` +- `Is heavy` +- `Room furni` +- `Wall furni` +- `Delayed events` +- `Average execution` +- `Peak execution` +- `Recursion` +- `Killed remaining` +- `Permanent furni vars` + +### 4.1.1 `Wired usage` + +Format: + +```text +usageCurrentWindow / usageLimitPerWindow +``` + +Source: + +- server-side `WiredRoomDiagnostics` + +Meaning: + +- `usageCurrentWindow` = cost consumed in the current rolling monitor window +- `usageLimitPerWindow` = max allowed budget before `EXECUTION_CAP` + +### 4.1.2 `Is heavy` + +Format: + +```text +Yes / No +``` + +Source: + +- server-side boolean from `WiredRoomDiagnostics` + +Meaning: + +- `Yes` if the room has crossed the heavy thresholds for enough consecutive windows + +### 4.1.3 `Room furni` + +Format: + +```text +(floor count + wall count) / roomItemLimit +``` + +Source: + +- numerator: renderer room object counts +- denominator: server room item limit exposed in room/session data + +### 4.1.4 `Wall furni` + +Format: + +```text +wall count / roomItemLimit +``` + +Important note: + +- there is **no separate wall-only cap** here +- the denominator is the same room furni limit exposed by the server + +### 4.1.5 `Delayed events` + +Format: + +```text +delayedEventsPending / delayedEventsLimit +``` + +Source: + +- server-side `WiredRoomDiagnostics` + +### 4.1.6 `Average execution` + +Format: + +```text +averageExecutionMs + "ms" +``` + +Meaning: + +- average execution time of sampled stacks inside the current monitor window + +### 4.1.7 `Peak execution` + +Format: + +```text +peakExecutionMs + "ms" +``` + +Meaning: + +- highest sampled execution time inside the current monitor window + +### 4.1.8 `Recursion` + +Format: + +```text +recursionDepthCurrent / recursionDepthLimit +``` + +Meaning: + +- current nested wired call depth vs the configured recursion cap + +### 4.1.9 `Killed remaining` + +Format: + +```text +killedRemainingSeconds + "s" +``` + +Meaning: + +- remaining room cooldown while wired execution is temporarily halted by protection logic + +### 4.1.10 `Permanent furni vars` + +Format: + +```text +customVariableEntryCount / 60 +``` + +Current meaning: + +- the numerator is the total number of renderer-side entries stored inside `RoomObjectVariable.FURNITURE_CUSTOM_VARIABLES` +- the denominator `60` is currently a fixed UI denominator + +This is currently **renderer-side custom variable count**, not a DB row count. + +--- + +## 4.2 Cost model behind `Wired usage` + +The current estimated stack cost is computed in the emulator. + +### 4.2.1 Base formula + +```text +cost = 1 +cost += number_of_conditions +cost += 2 for each selector effect +cost += 3 for each non-selector effect +cost += 4 extra for each delayed effect +cost += recursionDepth * 2 +cost = max(1, cost) +``` + +### 4.2.2 Practical breakdown + +| Element | Cost | +|---|---:| +| Base stack cost | `1` | +| Each condition | `+1` | +| Each selector effect | `+2` | +| Each regular effect | `+3` | +| Each delayed effect | `+4` extra | +| Each recursion level | `+2` | + +### 4.2.3 Example + +If a stack has: + +- `2` conditions +- `1` selector +- `2` normal effects +- `1` delayed effect +- recursion depth `1` + +Then: + +```text +1 ++ 2 conditions ++ 2 selector ++ 6 regular effects ++ 4 delayed effect extra ++ 2 recursion += 17 +``` + +That `17` is what is attempted against: + +```text +usageCurrentWindow + estimatedCost <= usageLimitPerWindow +``` + +If the result would exceed the budget, the engine records `EXECUTION_CAP`. + +--- + +## 4.3 Heavy / overload calculations + +### 4.3.1 Overload + +A monitor window is considered overloaded when: + +```text +executionSamplesCurrentWindow > 0 +AND ( + averageExecutionMs >= overloadAverageThresholdMs + OR + peakExecutionMs >= overloadPeakThresholdMs +) +``` + +After `wired.monitor.overload.consecutive.windows` consecutive overloaded windows: + +- the room logs `EXECUTOR_OVERLOAD` + +### 4.3.2 Heavy + +A monitor window is considered heavy when at least one of these is true: + +```text +usagePercent >= heavyUsageThresholdPercent +OR +delayedPercent >= heavyDelayedThresholdPercent +OR +overloadWindow == true +``` + +Where: + +```text +usagePercent = round(usageCurrentWindow * 100 / usageLimitPerWindow) +delayedPercent = round(delayedEventsPending * 100 / delayedEventsLimit) +``` + +After `wired.monitor.heavy.consecutive.windows` consecutive heavy windows: + +- the room is marked heavy +- the monitor logs `MARKED_AS_HEAVY` + +--- + +## 4.4 Error / warning log types + +The monitor currently supports: + +- `EXECUTION_CAP` +- `DELAYED_EVENTS_CAP` +- `EXECUTOR_OVERLOAD` +- `MARKED_AS_HEAVY` +- `KILLED` +- `RECURSION_TIMEOUT` + +Each log/history entry can carry: + +- type +- severity +- amount/count +- latest occurrence +- reason/motivation +- trigger/source label +- trigger/source id + +--- + +## 5. Inspection tab + +## 5.1 Furni inspection + +Current variables include: + +- `@id` +- `@class_id` +- `@height` +- `@state` +- `@position.x` +- `@position.y` +- `@rotation` +- `@altitude` +- `@wallitem_offset` (wall items only) +- `@type` +- `@dimensions.x` +- `@dimensions.y` +- `@owner_id` +- dynamic flags: + - `@can_sit_on` + - `@can_lay_on` + - `@can_stand_on` + - `@is_stackable` +- extra teleport variable when relevant: + - `~teleport.target_id` + +Editable fields: + +- `@state` +- `@position.x` +- `@position.y` +- `@rotation` +- `@altitude` +- `@wallitem_offset` (wall items) + +Important notes: + +- floor moves are sent through wired-style movement flow/animation +- wall item updates use wall position recomposition +- booleans such as sit/lay/stand/stack come from `items_base`-derived metadata, not from `FurnitureData.json` + +## 5.2 User inspection + +Current variables include: + +- `@index` +- `@type` +- `@gender` +- `@level` +- `@achievement_score` +- `@position.x` +- `@position.y` +- `@direction` +- `@altitude` +- `@favourite_group_id` +- `@room_entry` +- `@room_entry.teleport_id` +- `@user_id` / `@bot_id` / `@pet_id` + +Dynamic flags/actions include: + +- `@is_hc` +- `@has_rights` +- `@is_owner` +- `@is_group_admin` +- `@is_mute` +- `@is_trading` +- `@is_frozen` +- `@effect` +- `@team_score` +- `@team_color` +- `@team_type` +- `@sign` +- `@dance` +- `@is_idle` +- `@handitems` + +Editable fields: + +- `@position.x` +- `@position.y` +- `@direction` + +## 5.3 Global inspection + +Current variables include: + +- `@furni_count` +- `@user_count` +- `@wired_timer` +- `@teams.red.score` +- `@teams.green.score` +- `@teams.blue.score` +- `@teams.yellow.score` +- `@teams.red.size` +- `@teams.green.size` +- `@teams.blue.size` +- `@teams.yellow.size` +- `@room_id` +- `@group_id` +- `@timezone_server` +- `@timezone_client` +- `@current_time` +- `@current_time.millisecond_of_second` +- `@current_time.seconds_of_minute` +- `@current_time.minute_of_hour` +- `@current_time.hour_of_day` +- `@current_time.day_of_week` +- `@current_time.day_of_month` +- `@current_time.day_of_year` +- `@current_time.week_of_year` +- `@current_time.month_of_year` +- `@current_time.year` + +Important notes: + +- `@timezone_server` comes from the emulator room/session snapshot and follows `hotel.timezone` +- `@timezone_client` comes from the browser +- `@wired_timer` is currently client-side time since room entry +- `@current_time.*` is currently based on the server hotel time snapshot plus client-side progression + +--- + +## 6. UI behaviour notes + +### 6.1 Monitor windows + +The monitor now uses real Nitro card windows for: + +- info +- log history +- error information + +This means they are: + +- closable with the standard Nitro card close button +- draggable +- resizable + +### 6.2 Keep selected + +In `Inspection`: + +- when `Keep selected` is enabled +- clicking another furni/user does **not** replace the current preview/selection + +### 6.3 Inline editing + +Inline editors: + +- can be opened by clicking the row +- submit on `Enter` +- stop accidental room chat typing while the input is focused + +--- + +## 7. Current limitations + +- `Permanent furni vars` currently uses a fixed denominator (`60`) in the Nitro UI +- `@wired_timer` is still client-side, not a dedicated server timer +- `wired.tick.resolution` is kept for compatibility/documentation, but the current tick service uses `wired.tick.interval.ms` +- `wired.highscores.displaycount` is migrated/documented, but its usage should be validated in the current runtime path if highscore behaviour is changed later