diff --git a/Database Updates/001_optimize_gameserver.sql b/Database Updates/001_optimize_gameserver.sql index ffd8840a..88546176 100644 --- a/Database Updates/001_optimize_gameserver.sql +++ b/Database Updates/001_optimize_gameserver.sql @@ -1,1023 +1,92 @@ --- ============================================================================= --- Gameserver Database Optimization Migration --- ============================================================================= --- This migration optimizes the gameserver tables (not website_* tables). --- --- IMPORTANT: This script is designed to run on a POPULATED database safely. --- It uses IF NOT EXISTS / IF EXISTS where possible. --- --- What it does: --- 1. Converts Aria/MyISAM tables to InnoDB (required for foreign keys) --- 2. Fixes data type mismatches (unsigned/signed) so FKs can be created --- 3. Adds missing primary keys and indexes --- 4. Adds foreign key constraints for referential integrity --- --- BEFORE RUNNING: --- - Back up your database! --- - Run on a test environment first --- - The script disables FK checks during migration to avoid ordering issues --- ============================================================================= - SET FOREIGN_KEY_CHECKS = 0; SET @OLD_SQL_MODE = @@SQL_MODE; SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO'; --- ============================================================================= --- PHASE 1: Convert storage engines to InnoDB, ROW_FORMAT to DYNAMIC --- ============================================================================= --- Foreign keys require InnoDB. Converting Aria and MyISAM tables. --- InnoDB does not support ROW_FORMAT=FIXED, so all tables get ROW_FORMAT=DYNAMIC. --- Note: Aria tables lose PAGE_CHECKSUM (InnoDB has its own checksumming). - --- Core emulator tables ALTER TABLE IF EXISTS `achievements` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `bot_serves` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `catalog_clothing` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `catalog_club_offers` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `catalog_featured_pages` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `catalog_items_limited` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `chatlogs_private` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `chatlogs_room` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `commandlogs` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `emulator_errors` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; - --- Items & marketplace ALTER TABLE IF EXISTS `items` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `items_crackable` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `items_hoppers` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `items_presents` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `items_teleports` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `marketplace_items` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; - --- Navigator & rooms -ALTER TABLE IF EXISTS `navigator_publiccats` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `navigator_publics` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `navigator_filter` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `navigator_flatcats` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `nux_gifts` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; ALTER TABLE IF EXISTS `rooms` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `room_bans` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `room_enter_log` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `room_game_scores` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `room_models` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `room_models_custom` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `room_mutes` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `room_promotions` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `room_rights` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `room_votes` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `room_wordfilter` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; - --- Pets -ALTER TABLE IF EXISTS `pet_breeding` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `pet_breeding_races` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `pet_breeds` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `pet_drinks` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `pet_foods` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `pet_items` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; - --- Polls -ALTER TABLE IF EXISTS `polls` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `polls_answers` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `polls_questions` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; - --- Users ALTER TABLE IF EXISTS `users_achievements` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `users_achievements_queue` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `users_clothing` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `users_currency` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `users_effects` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `users_favorite_rooms` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `users_navigator_settings` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `users_pets` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `users_recipes` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `user_window_settings` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; - --- Misc -ALTER TABLE IF EXISTS `crafting_altars_recipes` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `crafting_recipes` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `crafting_recipes_ingredients` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `namechange_log` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `recycler_prizes` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `special_enables` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `vouchers` 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 `wired_rewards_given` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `youtube_playlists` ENGINE = InnoDB, ROW_FORMAT = DYNAMIC; -ALTER TABLE IF EXISTS `bots` 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`; - --- ============================================================================= --- PHASE 2: Fix data type mismatches (unsigned vs signed) --- ============================================================================= --- Foreign keys require EXACT type matches including signedness. --- Fix columns where referenced PK and referencing FK differ. - --- 2a. users.id is int(11) SIGNED → fix unsigned user_id columns -ALTER TABLE IF EXISTS `logs_hc_payday` - MODIFY `user_id` int(11) DEFAULT NULL; - -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; - --- 2b. items_base.id is int(11) UNSIGNED → fix signed FK columns to match -ALTER TABLE IF EXISTS `items` - MODIFY `item_id` int(11) unsigned DEFAULT 0; - -ALTER TABLE IF EXISTS `economy_furniture` - MODIFY `items_base_id` int(11) unsigned NOT NULL; - -ALTER TABLE IF EXISTS `items_crackable` - MODIFY `item_id` int(11) unsigned NOT NULL; - --- 2c. guilds_forums_threads.id is int(10) UNSIGNED → fix signed FK columns -ALTER TABLE IF EXISTS `guilds_forums_comments` - MODIFY `thread_id` int(10) unsigned NOT NULL DEFAULT 0; - - --- ============================================================================= --- PHASE 3: Add missing primary keys --- ============================================================================= --- Tables without primary keys hurt performance and replication. - --- bot_serves: no PK -ALTER TABLE IF EXISTS `bot_serves` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- chatlogs_room: no PK (high-volume log table, add auto-increment PK) -ALTER TABLE IF EXISTS `chatlogs_room` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- commandlogs: no PK -ALTER TABLE IF EXISTS `commandlogs` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- crafting_recipes_ingredients: no PK -ALTER TABLE IF EXISTS `crafting_recipes_ingredients` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- items_hoppers: no PK -ALTER TABLE IF EXISTS `items_hoppers` - ADD PRIMARY KEY (`item_id`); - --- items_presents: no PK -ALTER TABLE IF EXISTS `items_presents` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- items_teleports: no PK -ALTER TABLE IF EXISTS `items_teleports` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- namechange_log: no PK -ALTER TABLE IF EXISTS `namechange_log` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- navigator_publics: no PK -ALTER TABLE IF EXISTS `navigator_publics` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- pet_breeding: no PK -ALTER TABLE IF EXISTS `pet_breeding` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- pet_breeding_races: no PK -ALTER TABLE IF EXISTS `pet_breeding_races` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- pet_drinks: no PK -ALTER TABLE IF EXISTS `pet_drinks` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- pet_foods: no PK -ALTER TABLE IF EXISTS `pet_foods` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- pet_items: no PK -ALTER TABLE IF EXISTS `pet_items` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- pet_vocals: no PK -ALTER TABLE IF EXISTS `pet_vocals` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- recycler_prizes: no PK -ALTER TABLE IF EXISTS `recycler_prizes` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- room_bans: no PK -ALTER TABLE IF EXISTS `room_bans` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- room_enter_log: no PK -ALTER TABLE IF EXISTS `room_enter_log` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- room_game_scores: no PK -ALTER TABLE IF EXISTS `room_game_scores` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- room_mutes: no PK -ALTER TABLE IF EXISTS `room_mutes` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- room_rights: no PK (use composite) -ALTER TABLE IF EXISTS `room_rights` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- room_trax: no PK -ALTER TABLE IF EXISTS `room_trax` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- room_trax_playlist: no PK -ALTER TABLE IF EXISTS `room_trax_playlist` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- room_votes: no PK (use composite unique) -ALTER TABLE IF EXISTS `room_votes` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- trax_playlist: no PK -ALTER TABLE IF EXISTS `trax_playlist` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- wired_rewards_given: no PK -ALTER TABLE IF EXISTS `wired_rewards_given` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- calendar_rewards_claimed: no PK -ALTER TABLE IF EXISTS `calendar_rewards_claimed` - ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; - --- camera: no PK -ALTER TABLE IF EXISTS `camera` - MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, - ADD PRIMARY KEY (`id`); - - --- ============================================================================= --- PHASE 4: Add missing indexes --- ============================================================================= --- Adding indexes for columns commonly used in JOINs and WHERE clauses. - --- bans: index on user_id for lookups -ALTER TABLE IF EXISTS `bans` - ADD INDEX `idx_bans_user_id` (`user_id`), - ADD INDEX `idx_bans_ip` (`ip`), - ADD INDEX `idx_bans_machine_id` (`machine_id`(64)); - --- calendar_rewards: index on campaign_id -ALTER TABLE IF EXISTS `calendar_rewards` - ADD INDEX `idx_calendar_rewards_campaign_id` (`campaign_id`); - --- calendar_rewards_claimed: indexes for lookups -ALTER TABLE IF EXISTS `calendar_rewards_claimed` - ADD INDEX `idx_cal_claimed_user_id` (`user_id`), - ADD INDEX `idx_cal_claimed_campaign_id` (`campaign_id`), - ADD INDEX `idx_cal_claimed_reward_id` (`reward_id`); - --- camera: indexes -ALTER TABLE IF EXISTS `camera` - ADD INDEX `idx_camera_user_id` (`user_id`), - ADD INDEX `idx_camera_room_id` (`room_id`); - --- guilds: index on user_id -ALTER TABLE IF EXISTS `guilds` - ADD INDEX `idx_guilds_user_id` (`user_id`), - ADD INDEX `idx_guilds_room_id` (`room_id`); - --- guilds_forums_threads: index on guild_id -ALTER TABLE IF EXISTS `guilds_forums_threads` - ADD INDEX `idx_gft_guild_id` (`guild_id`), - ADD INDEX `idx_gft_opener_id` (`opener_id`); - --- guilds_forums_comments: index on user_id -ALTER TABLE IF EXISTS `guilds_forums_comments` - ADD INDEX `idx_gfc_user_id` (`user_id`); - --- guild_forum_views: indexes -ALTER TABLE IF EXISTS `guild_forum_views` - ADD INDEX `idx_gfv_user_id` (`user_id`), - ADD INDEX `idx_gfv_guild_id` (`guild_id`); - --- items_crackable: index on item_id -ALTER TABLE IF EXISTS `items_crackable` - ADD INDEX `idx_items_crackable_item_id` (`item_id`); - --- namechange_log: index on user_id -ALTER TABLE IF EXISTS `namechange_log` - ADD INDEX `idx_namechange_user_id` (`user_id`); - --- messenger_friendrequests: indexes -ALTER TABLE IF EXISTS `messenger_friendrequests` - ADD INDEX `idx_fr_user_to_id` (`user_to_id`), - ADD INDEX `idx_fr_user_from_id` (`user_from_id`); - --- room_bans: indexes -ALTER TABLE IF EXISTS `room_bans` - ADD INDEX `idx_room_bans_room_id` (`room_id`), - ADD INDEX `idx_room_bans_user_id` (`user_id`); - --- room_mutes: indexes -ALTER TABLE IF EXISTS `room_mutes` - ADD INDEX `idx_room_mutes_room_id` (`room_id`), - ADD INDEX `idx_room_mutes_user_id` (`user_id`); - --- room_votes: index on room_id -ALTER TABLE IF EXISTS `room_votes` - ADD INDEX `idx_room_votes_room_id` (`room_id`); - --- sanctions: index on habbo_id -ALTER TABLE IF EXISTS `sanctions` - ADD INDEX `idx_sanctions_habbo_id` (`habbo_id`); - --- shadowbans: index on user_id -ALTER TABLE IF EXISTS `shadowbans` - ADD INDEX `idx_shadowbans_user_id` (`user_id`); - --- support_cfh_topics: index on category_id -ALTER TABLE IF EXISTS `support_cfh_topics` - ADD INDEX `idx_cfh_topics_category_id` (`category_id`); - --- support_tickets: indexes -ALTER TABLE IF EXISTS `support_tickets` - ADD INDEX `idx_tickets_sender_id` (`sender_id`), - ADD INDEX `idx_tickets_reported_id` (`reported_id`), - ADD INDEX `idx_tickets_mod_id` (`mod_id`), - ADD INDEX `idx_tickets_room_id` (`room_id`); - --- users_settings: already has user_id index, good - --- voucher_history: indexes -ALTER TABLE IF EXISTS `voucher_history` - ADD INDEX `idx_vh_voucher_id` (`voucher_id`), - ADD INDEX `idx_vh_user_id` (`user_id`); - --- ls_name_backgrounds_owned -ALTER TABLE IF EXISTS `ls_name_backgrounds_owned` - ADD INDEX `idx_lsnbo_user_id` (`user_id`), - ADD INDEX `idx_lsnbo_bg_id` (`name_background_id`); - --- ls_name_colors_owned -ALTER TABLE IF EXISTS `ls_name_colors_owned` - ADD INDEX `idx_lsnco_user_id` (`user_id`), - ADD INDEX `idx_lsnco_color_id` (`name_color_id`); - --- ls_prefixes_owned -ALTER TABLE IF EXISTS `ls_prefixes_owned` - ADD INDEX `idx_lspo_user_id` (`user_id`), - ADD INDEX `idx_lspo_prefix_id` (`prefix_id`); - --- users_target_offer_purchases -ALTER TABLE IF EXISTS `users_target_offer_purchases` - ADD INDEX `idx_utop_offer_id` (`offer_id`); - --- users_unlockable_commands -ALTER TABLE IF EXISTS `users_unlockable_commands` - ADD INDEX `idx_uuc_user_id` (`user_id`); - --- command_category_permissions: index on category_id -ALTER TABLE IF EXISTS `command_category_permissions` - ADD INDEX `idx_ccp_category_id` (`category_id`); - - --- ============================================================================= --- PHASE 5: Foreign key constraints --- ============================================================================= --- Adding FK constraints for referential integrity. --- Using appropriate ON DELETE actions: --- CASCADE = child rows deleted when parent is deleted --- SET NULL = child FK set to NULL when parent is deleted (column must be nullable) --- RESTRICT = prevent parent deletion if children exist (default) --- --- NOTE: Log/archive tables (chatlogs, commandlogs, room_enter_log, etc.) --- intentionally do NOT get FKs to avoid cascade-deleting historical data --- and to keep high-volume inserts fast. --- ============================================================================= - --- --------------------------------------------------------------------------- --- 5.1 Rooms → Users (owner) --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `rooms` - ADD CONSTRAINT `fk_rooms_owner` - FOREIGN KEY (`owner_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.2 Items → Users, Rooms, Items_base --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `items` - ADD CONSTRAINT `fk_items_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_items_item_base` - FOREIGN KEY (`item_id`) REFERENCES `items_base` (`id`) - ON DELETE CASCADE; - --- Note: items.room_id = 0 means "in inventory", so we can't FK to rooms --- unless we allow NULL instead of 0. Skipping this FK. - --- --------------------------------------------------------------------------- --- 5.3 Bots → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `bots` - ADD CONSTRAINT `fk_bots_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.4 Bans → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `bans` - ADD CONSTRAINT `fk_bans_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_bans_staff` - FOREIGN KEY (`user_staff_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.5 Guilds → Users, Rooms --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `guilds` - ADD CONSTRAINT `fk_guilds_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_guilds_room` - FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.6 Guild members → Guilds, Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `guilds_members` - ADD CONSTRAINT `fk_gm_guild` - FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_gm_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.7 Guild forum threads → Guilds, Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `guilds_forums_threads` - ADD CONSTRAINT `fk_gft_guild` - FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_gft_opener` - FOREIGN KEY (`opener_id`) REFERENCES `users` (`id`) - ON DELETE SET NULL; - --- --------------------------------------------------------------------------- --- 5.8 Guild forum comments → Threads, Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `guilds_forums_comments` - ADD CONSTRAINT `fk_gfc_thread` - FOREIGN KEY (`thread_id`) REFERENCES `guilds_forums_threads` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_gfc_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.9 Guild forum views → Guilds, Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `guild_forum_views` - ADD CONSTRAINT `fk_gfv_guild` - FOREIGN KEY (`guild_id`) REFERENCES `guilds` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_gfv_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.10 Catalog items → Catalog pages --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `catalog_items` - ADD CONSTRAINT `fk_catitems_page` - FOREIGN KEY (`page_id`) REFERENCES `catalog_pages` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.11 Catalog items limited → Catalog items --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `catalog_items_limited` - ADD CONSTRAINT `fk_catitemsltd_catitem` - FOREIGN KEY (`catalog_item_id`) REFERENCES `catalog_items` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.12 Calendar rewards → Calendar campaigns --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `calendar_rewards` - ADD CONSTRAINT `fk_calrewards_campaign` - FOREIGN KEY (`campaign_id`) REFERENCES `calendar_campaigns` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.13 Calendar rewards claimed → Users, Campaigns, Rewards --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `calendar_rewards_claimed` - ADD CONSTRAINT `fk_calclaimed_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_calclaimed_campaign` - FOREIGN KEY (`campaign_id`) REFERENCES `calendar_campaigns` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_calclaimed_reward` - FOREIGN KEY (`reward_id`) REFERENCES `calendar_rewards` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.14 Users_settings → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_settings` - ADD CONSTRAINT `fk_usettings_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.15 Users_badges → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_badges` - ADD CONSTRAINT `fk_ubadges_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.16 Users_currency → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_currency` - ADD CONSTRAINT `fk_ucurrency_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.17 Users_effects → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_effects` - ADD CONSTRAINT `fk_ueffects_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.18 Users_clothing → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_clothing` - ADD CONSTRAINT `fk_uclothing_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.19 Users_favorite_rooms → Users, Rooms --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_favorite_rooms` - ADD CONSTRAINT `fk_ufavrooms_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_ufavrooms_room` - FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.20 Users_wardrobe → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_wardrobe` - ADD CONSTRAINT `fk_uwardrobe_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.21 Users_pets → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_pets` - ADD CONSTRAINT `fk_upets_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.22 Users_recipes → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_recipes` - ADD CONSTRAINT `fk_urecipes_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.23 Users_saved_searches → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_saved_searches` - ADD CONSTRAINT `fk_usavedsearches_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.24 Users_navigator_settings → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_navigator_settings` - ADD CONSTRAINT `fk_unavsettings_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.25 Users_achievements → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_achievements` - ADD CONSTRAINT `fk_uachievements_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.26 Users_achievements_queue → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_achievements_queue` - ADD CONSTRAINT `fk_uachqueue_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.27 Users_ignored → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_ignored` - ADD CONSTRAINT `fk_uignored_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_uignored_target` - FOREIGN KEY (`target_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.28 Users_subscriptions → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_subscriptions` - ADD CONSTRAINT `fk_usubs_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.29 Users_target_offer_purchases → Users, Catalog target offers --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_target_offer_purchases` - ADD CONSTRAINT `fk_utop_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_utop_offer` - FOREIGN KEY (`offer_id`) REFERENCES `catalog_target_offers` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.30 Users_unlockable_commands → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `users_unlockable_commands` - ADD CONSTRAINT `fk_uunlockable_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.31 User_window_settings → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `user_window_settings` - ADD CONSTRAINT `fk_uwinsettings_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.32 User_prefixes → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `user_prefixes` - ADD CONSTRAINT `fk_uprefixes_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.33 Messenger_friendships → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `messenger_friendships` - ADD CONSTRAINT `fk_mfriends_user_one` - FOREIGN KEY (`user_one_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_mfriends_user_two` - FOREIGN KEY (`user_two_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.34 Messenger_friendrequests → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `messenger_friendrequests` - ADD CONSTRAINT `fk_mfr_user_to` - FOREIGN KEY (`user_to_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_mfr_user_from` - FOREIGN KEY (`user_from_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.35 Messenger_categories → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `messenger_categories` - ADD CONSTRAINT `fk_mcat_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.36 Marketplace_items → Items, Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `marketplace_items` - ADD CONSTRAINT `fk_market_item` - FOREIGN KEY (`item_id`) REFERENCES `items` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_market_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.37 Room_rights → Rooms, Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `room_rights` - ADD CONSTRAINT `fk_rrights_room` - FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_rrights_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.38 Room_bans → Rooms, Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `room_bans` - ADD CONSTRAINT `fk_rbans_room` - FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_rbans_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.39 Room_mutes → Rooms, Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `room_mutes` - ADD CONSTRAINT `fk_rmutes_room` - FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_rmutes_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.40 Room_votes → Rooms, Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `room_votes` - ADD CONSTRAINT `fk_rvotes_room` - FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_rvotes_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.41 Room_wordfilter → Rooms --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `room_wordfilter` - ADD CONSTRAINT `fk_rwf_room` - FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.42 Room_promotions → Rooms --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `room_promotions` - ADD CONSTRAINT `fk_rpromo_room` - FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.43 Room_trax → Rooms --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `room_trax` - ADD CONSTRAINT `fk_rtrax_room` - FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.44 Room_trax_playlist → Rooms --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `room_trax_playlist` - ADD CONSTRAINT `fk_rtraxpl_room` - FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.45 Rooms_for_sale → Rooms, Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `rooms_for_sale` - ADD CONSTRAINT `fk_r4sale_room` - FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_r4sale_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.46 Room_trade_log → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `room_trade_log` - ADD CONSTRAINT `fk_rtlog_user_one` - FOREIGN KEY (`user_one_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_rtlog_user_two` - FOREIGN KEY (`user_two_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.47 Room_trade_log_items → Room_trade_log, Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `room_trade_log_items` - ADD CONSTRAINT `fk_rtli_trade` - FOREIGN KEY (`id`) REFERENCES `room_trade_log` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_rtli_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.48 Polls_questions → Polls --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `polls_questions` - ADD CONSTRAINT `fk_pq_poll` - FOREIGN KEY (`poll_id`) REFERENCES `polls` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.49 Polls_answers → Polls, Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `polls_answers` - ADD CONSTRAINT `fk_pa_poll` - FOREIGN KEY (`poll_id`) REFERENCES `polls` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_pa_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.50 Crafting_recipes_ingredients → Crafting_recipes, Items_base --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `crafting_recipes_ingredients` - ADD CONSTRAINT `fk_cri_recipe` - FOREIGN KEY (`recipe_id`) REFERENCES `crafting_recipes` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.51 Crafting_altars_recipes → Crafting_recipes --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `crafting_altars_recipes` - ADD CONSTRAINT `fk_car_recipe` - FOREIGN KEY (`recipe_id`) REFERENCES `crafting_recipes` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.52 Voucher_history → Vouchers, Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `voucher_history` - ADD CONSTRAINT `fk_vh_voucher` - FOREIGN KEY (`voucher_id`) REFERENCES `vouchers` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_vh_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.53 Support_cfh_topics → Support_cfh_categories --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `support_cfh_topics` - ADD CONSTRAINT `fk_cfhtopics_category` - FOREIGN KEY (`category_id`) REFERENCES `support_cfh_categories` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.54 Command_category_permissions → Command_categories --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `command_category_permissions` - ADD CONSTRAINT `fk_ccp_category` - FOREIGN KEY (`category_id`) REFERENCES `command_categories` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.55 Economy_furniture → Items_base, Economy_categories --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `economy_furniture` - ADD CONSTRAINT `fk_econfurni_itembase` - FOREIGN KEY (`items_base_id`) REFERENCES `items_base` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_econfurni_category` - FOREIGN KEY (`economy_categories_id`) REFERENCES `economy_categories` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.56 Sanctions → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `sanctions` - ADD CONSTRAINT `fk_sanctions_user` - FOREIGN KEY (`habbo_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.57 Camera_web → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `camera_web` - ADD CONSTRAINT `fk_cameraweb_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.58 LS ownership tables → Users, LS definition tables --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `ls_name_backgrounds_owned` - ADD CONSTRAINT `fk_lsnbo_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_lsnbo_bg` - FOREIGN KEY (`name_background_id`) REFERENCES `ls_name_backgrounds` (`id`) - ON DELETE CASCADE; - -ALTER TABLE IF EXISTS `ls_name_colors_owned` - ADD CONSTRAINT `fk_lsnco_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_lsnco_color` - FOREIGN KEY (`name_color_id`) REFERENCES `ls_name_colors` (`id`) - ON DELETE CASCADE; - -ALTER TABLE IF EXISTS `ls_prefixes_owned` - ADD CONSTRAINT `fk_lspo_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_lspo_prefix` - FOREIGN KEY (`prefix_id`) REFERENCES `ls_prefixes` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.59 Navigator_publics → Navigator_publiccats, Rooms --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `navigator_publics` - ADD CONSTRAINT `fk_navpub_cat` - FOREIGN KEY (`public_cat_id`) REFERENCES `navigator_publiccats` (`id`) - ON DELETE CASCADE, - ADD CONSTRAINT `fk_navpub_room` - FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) - ON DELETE CASCADE; - --- --------------------------------------------------------------------------- --- 5.60 GOTW winners → Users --- --------------------------------------------------------------------------- -ALTER TABLE IF EXISTS `gotw_winners` - ADD CONSTRAINT `fk_gotw_user` - FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) - ON DELETE CASCADE; - - --- ============================================================================= --- PHASE 6: Charset standardization --- ============================================================================= --- Standardize remaining utf8mb3 tables to utf8mb4 for full Unicode support. - -ALTER TABLE IF EXISTS `guilds` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE IF EXISTS `guilds_elements` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE IF EXISTS `groups_items` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE IF EXISTS `messenger_friendships` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE IF EXISTS `room_rights` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE IF EXISTS `soundtracks` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE IF EXISTS `users_achievements_queue` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE IF EXISTS `users_saved_searches` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE IF EXISTS `users_target_offer_purchases` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE IF EXISTS `wordfilter` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE IF EXISTS `logs_shop_purchases` - CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - - --- ============================================================================= --- Done - Re-enable foreign key checks --- ============================================================================= SET FOREIGN_KEY_CHECKS = 1; -SET SQL_MODE = @OLD_SQL_MODE; +SET SQL_MODE = @OLD_SQL_MODE; \ No newline at end of file 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/006_HabboManager_fix.sql b/Database Updates/006_HabboManager_fix.sql new file mode 100644 index 00000000..971c0436 --- /dev/null +++ b/Database Updates/006_HabboManager_fix.sql @@ -0,0 +1,71 @@ +ALTER TABLE `users` DROP KEY IF EXISTS `auth_ticket`; +ALTER TABLE `users` + MODIFY `auth_ticket` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT ''; +CREATE INDEX IF NOT EXISTS `idx_users_auth_ticket` ON `users` (`auth_ticket`); +CREATE INDEX IF NOT EXISTS `idx_rel_user_room` ON `room_enter_log` (`user_id`, `room_id`); +CREATE INDEX IF NOT EXISTS `idx_lhcp_user_claimed` ON `logs_hc_payday` (`user_id`, `claimed`); +CREATE UNIQUE INDEX IF NOT EXISTS `uniq_room_votes_user_room` ON `room_votes` (`user_id`, `room_id`); +ALTER TABLE `room_votes` DROP KEY IF EXISTS `user_id`; +CREATE INDEX IF NOT EXISTS `idx_rgs_room_ts` ON `room_game_scores` (`room_id`, `game_start_timestamp`); +CREATE INDEX IF NOT EXISTS `idx_rgs_user` ON `room_game_scores` (`user_id`); +CREATE UNIQUE INDEX IF NOT EXISTS `uniq_crc_user_campaign_reward` + ON `calendar_rewards_claimed` (`user_id`, `campaign_id`, `reward_id`); +ALTER TABLE `calendar_rewards_claimed` DROP KEY IF EXISTS `idx_cal_claimed_user_id`; +ALTER TABLE `emulator_settings` + ENGINE = InnoDB, + CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +DROP TABLE IF EXISTS `gift_wrappers_new`; +CREATE TABLE `gift_wrappers_new` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `sprite_id` int(11) NOT NULL, + `item_id` int(11) NOT NULL, + `type` enum('gift','wrapper') NOT NULL DEFAULT 'wrapper', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +INSERT INTO `gift_wrappers_new` (`id`, `sprite_id`, `item_id`, `type`) + SELECT `id`, `sprite_id`, `item_id`, `type` FROM `gift_wrappers`; +DROP TABLE `gift_wrappers`; +RENAME TABLE `gift_wrappers_new` TO `gift_wrappers`; + + +DROP TABLE IF EXISTS `pet_actions_new`; +CREATE TABLE `pet_actions_new` ( + `pet_type` int(2) NOT NULL AUTO_INCREMENT, + `pet_name` varchar(32) NOT NULL, + `offspring_type` int(3) NOT NULL DEFAULT -1, + `happy_actions` varchar(100) NOT NULL DEFAULT '', + `tired_actions` varchar(100) NOT NULL DEFAULT '', + `random_actions` varchar(100) NOT NULL DEFAULT '', + `can_swim` enum('1','0') DEFAULT '0', + PRIMARY KEY (`pet_type`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +INSERT INTO `pet_actions_new` + (`pet_type`, `pet_name`, `offspring_type`, `happy_actions`, `tired_actions`, `random_actions`, `can_swim`) + SELECT `pet_type`, `pet_name`, `offspring_type`, `happy_actions`, `tired_actions`, `random_actions`, `can_swim` + FROM `pet_actions`; +DROP TABLE `pet_actions`; +RENAME TABLE `pet_actions_new` TO `pet_actions`; + + +DROP TABLE IF EXISTS `pet_commands_data_new`; +CREATE TABLE `pet_commands_data_new` ( + `command_id` int(3) NOT NULL, + `text` varchar(25) NOT NULL, + `required_level` int(2) NOT NULL, + `reward_xp` int(3) NOT NULL DEFAULT 5, + `cost_happiness` int(11) NOT NULL DEFAULT 0, + `cost_energy` int(3) NOT NULL DEFAULT 0, + PRIMARY KEY (`command_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +INSERT INTO `pet_commands_data_new` + (`command_id`, `text`, `required_level`, `reward_xp`, `cost_happiness`, `cost_energy`) + SELECT `command_id`, `text`, `required_level`, `reward_xp`, `cost_happiness`, `cost_energy` + FROM `pet_commands_data`; +DROP TABLE `pet_commands_data`; +RENAME TABLE `pet_commands_data_new` TO `pet_commands_data`; + +ALTER TABLE `calendar_rewards` + MODIFY `product_name` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + MODIFY `custom_image` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + MODIFY `badge` VARCHAR(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + MODIFY `subscription_type` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT ''; 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..1d57887f --- /dev/null +++ b/Database Updates/Items_Base/update_all_interaction_types_wired.sql @@ -0,0 +1,158 @@ +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` = '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_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/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/users/HabboManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboManager.java index 1ed19667..8fc6aeb6 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 @@ -198,7 +198,7 @@ public class HabboManager { 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 ?")) { + 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()); 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 69b29a69..5a96d607 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 @@ -42,7 +42,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. @@ -52,14 +52,14 @@ import java.util.concurrent.ConcurrentHashMap; *
  3. Execute effects (respecting random/unseen modifiers)
  4. *
  5. Handle delays for timed effects
  6. *
- * + * *

Safety Features:

* - * + * * @see WiredEvent * @see WiredContext * @see WiredStackIndex @@ -67,16 +67,16 @@ 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; @@ -110,25 +110,28 @@ public final class WiredEngine { 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) @@ -137,7 +140,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; @@ -146,11 +149,12 @@ public final class WiredEngine { 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) */ @@ -163,20 +167,14 @@ 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) { @@ -192,7 +190,7 @@ public final class WiredEngine { return false; } roomRecursionDepth.put(roomId, currentDepth + 1); - + try { return handleEventInternal(event, room); } finally { @@ -205,7 +203,129 @@ 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. */ @@ -284,8 +404,8 @@ public final class WiredEngine { String monitorSourceLabel = getMonitorSourceLabel(stack.triggerItem(), event); int monitorSourceId = getMonitorSourceId(stack.triggerItem()); - debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})", - event.getType(), + debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})", + event.getType(), stack.triggerItem() != null ? stack.triggerItem().getId() : "null", stack.conditions().size(), stack.effects().size()); @@ -520,11 +640,11 @@ public final class WiredEngine { */ private void executeEffects(WiredStack stack, WiredContext ctx, long currentTime) { List effects = stack.effects(); - + if (effects.isEmpty()) { return; } - + // Selectors already executed before conditions; only run regular effects here List regulars = new ArrayList<>(); for (IWiredEffect e : effects) { @@ -698,7 +818,7 @@ public final class WiredEngine { WiredSelectionFilterSupport.applySelectorFilters(room, stack.triggerItem(), ctx); } - + /** * Schedule a delayed effect execution. */ @@ -722,16 +842,16 @@ public final class WiredEngine { 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; @@ -895,14 +1015,14 @@ public final class WiredEngine { * 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; } @@ -915,7 +1035,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) { @@ -927,7 +1047,7 @@ public final class WiredEngine { legacyConditions.add((InteractionWiredCondition) cond); } } - + WiredStackTriggeredEvent triggeredEvent = new WiredStackTriggeredEvent( event.getRoom(), event.getActor().orElse(null), @@ -935,7 +1055,7 @@ public final class WiredEngine { legacyEffects, legacyConditions ); - + return !Emulator.getPluginManager().fireEvent(triggeredEvent).isCancelled(); } return true; @@ -948,7 +1068,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); @@ -959,7 +1079,7 @@ public final class WiredEngine { legacyConditions.add((InteractionWiredCondition) cond); } } - + Emulator.getPluginManager().fireEvent(new WiredStackExecutedEvent( event.getRoom(), event.getActor().orElse(null), @@ -974,10 +1094,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); } /** @@ -987,10 +1113,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); @@ -1068,7 +1194,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. @@ -1077,14 +1203,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 @@ -1093,7 +1219,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. @@ -1126,10 +1252,45 @@ public final class WiredEngine { 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) { @@ -1152,7 +1313,7 @@ public final class WiredEngine { now ); } - + /** * Check if a room is currently banned from wired execution. * @param roomId the room ID @@ -1163,21 +1324,19 @@ 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, WiredEvent.Type eventType, int eventCount) { long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS; @@ -1213,19 +1372,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); @@ -1233,7 +1392,7 @@ 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 @@ -1340,43 +1499,38 @@ public final class WiredEngine { 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; 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 8949e0d8..5a02b34a 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 @@ -57,6 +57,14 @@ import java.util.ArrayDeque; *
  • {@code wired.engine.debug} - Verbose logging
  • * * + *

    Migration Strategy:

    + *
      + *
    1. Set {@code wired.engine.enabled=true} to run both engines in parallel
    2. + *
    3. Test thoroughly to ensure identical behavior
    4. + *
    5. Set {@code wired.engine.exclusive=true} to disable legacy engine
    6. + *
    7. Full migration complete - WiredManager is now the only wired engine
    8. + *
    + * * @see WiredEngine * @see WiredEvents */ @@ -80,10 +88,10 @@ public final class WiredManager { /** 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<>(); @@ -116,7 +124,7 @@ public final class WiredManager { 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); @@ -130,7 +138,7 @@ 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(); @@ -140,8 +148,8 @@ public final class WiredManager { 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 - exclusive runtime active, maxSteps: {}, debug: {}", - maxSteps, debug); + LOGGER.info("Wired Manager initialized - enabled: {}, exclusive runtime active, maxSteps: {}, debug: {}", + enabled, maxSteps, debug); } /** @@ -154,17 +162,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; @@ -300,6 +309,18 @@ public final class WiredManager { 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()); + } + /** * Trigger when a user walks onto furniture. */ @@ -307,7 +328,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); } @@ -319,7 +340,7 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null || item == null) { return false; } - + WiredEvent event = WiredEvents.userWalksOff(room, user, item); return handleEvent(event); } @@ -428,7 +449,7 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null) { return false; } - + WiredEvent event = WiredEvents.userEntersRoom(room, user); return handleEvent(event); } @@ -452,7 +473,7 @@ public final class WiredManager { if (!isEnabled() || room == null || item == null) { return false; } - + WiredEvent event = WiredEvents.furniStateChanged(room, user, item); return handleEvent(event); } @@ -491,24 +512,24 @@ public final class WiredManager { * 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) { @@ -517,31 +538,31 @@ public final class WiredManager { } WiredEvent event = WiredEvents.clockCounter(room, counterItem); - return handleEvent(event); + return handleEventForSourceItem(event, counterItem); } /** * Trigger a long periodic timer. */ public static boolean triggerTimerRepeatLong(Room room, HabboItem timerItem) { - if (!isEnabled() || room == null) { + if (!isEnabled() || room == null || timerItem == null) { return false; } WiredEvent event = WiredEvents.timerRepeatLong(room, timerItem); - return handleEvent(event); + return handleEventForSourceItem(event, timerItem); } /** * Trigger a short periodic timer. */ public static boolean triggerTimerRepeatShort(Room room, HabboItem timerItem) { - if (!isEnabled() || room == null) { + if (!isEnabled() || room == null || timerItem == null) { return false; } WiredEvent event = WiredEvents.timerRepeatShort(room, timerItem); - return handleEvent(event); + return handleEventForSourceItem(event, timerItem); } /** @@ -551,7 +572,7 @@ public final class WiredManager { if (!isEnabled() || room == null) { return false; } - + WiredEvent event = WiredEvents.gameStarts(room); return handleEvent(event); } @@ -563,7 +584,7 @@ public final class WiredManager { if (!isEnabled() || room == null) { return false; } - + WiredEvent event = WiredEvents.gameEnds(room); return handleEvent(event); } @@ -575,7 +596,7 @@ public final class WiredManager { if (!isEnabled() || room == null || botUnit == null) { return false; } - + WiredEvent event = WiredEvents.botCollision(room, botUnit); return handleEvent(event); } @@ -587,7 +608,7 @@ public final class WiredManager { if (!isEnabled() || room == null || botUnit == null) { return false; } - + WiredEvent event = WiredEvents.botReachedFurni(room, botUnit, item); return handleEvent(event); } @@ -599,7 +620,7 @@ public final class WiredManager { if (!isEnabled() || room == null || botUnit == null) { return false; } - + WiredEvent event = WiredEvents.botReachedHabbo(room, botUnit, targetUser); return handleEvent(event); } @@ -615,7 +636,7 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null) { return false; } - + WiredEvent event = WiredEvents.scoreAchieved(room, user, score, scoreAdded); return handleEvent(event); } @@ -627,7 +648,7 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null) { return false; } - + WiredEvent event = WiredEvents.userIdles(room, user); return handleEvent(event); } @@ -639,7 +660,7 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null) { return false; } - + WiredEvent event = WiredEvents.userUnidles(room, user); return handleEvent(event); } @@ -651,7 +672,7 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null) { return false; } - + WiredEvent event = WiredEvents.userStartsDancing(room, user); return handleEvent(event); } @@ -663,7 +684,7 @@ public final class WiredManager { if (!isEnabled() || room == null || user == null) { return false; } - + WiredEvent event = WiredEvents.userStopsDancing(room, user); return handleEvent(event); } @@ -675,7 +696,7 @@ public final class WiredManager { if (!isEnabled() || room == null) { return false; } - + WiredEvent event = WiredEvents.teamWins(room, user); return handleEvent(event); } @@ -687,7 +708,7 @@ public final class WiredManager { if (!isEnabled() || room == null) { return false; } - + WiredEvent event = WiredEvents.teamLoses(room, user); return handleEvent(event); } @@ -700,7 +721,7 @@ public final class WiredManager { if (!isEnabled() || room == null) { return false; } - + WiredEvent event = WiredEvents.fromLegacy(triggerType, room, roomUnit, stuff); return handleEvent(event); } @@ -712,11 +733,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()); } } @@ -727,13 +757,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); } } @@ -742,19 +784,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) { @@ -763,19 +805,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 */ @@ -786,7 +828,7 @@ public final class WiredManager { } // ========== JSON Utilities ========== - + private static GsonBuilder gsonBuilder = null; private static Gson cachedGson = null; @@ -796,12 +838,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() { @@ -812,55 +854,58 @@ 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() { @@ -902,7 +947,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) { @@ -935,9 +980,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); @@ -954,12 +999,12 @@ public final class WiredManager { /** * 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(); @@ -1197,4 +1242,3 @@ public final class WiredManager { return false; } } - 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/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/rooms/users/RoomUserWalkEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java index dba30ec8..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 @@ -26,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 @@ -37,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) { diff --git a/Latest_Compiled_Version/Habbo-4.1.0-jar-with-dependencies.jar b/Latest_Compiled_Version/Habbo-4.1.0-jar-with-dependencies.jar index 94c084b2..60f89e8f 100644 Binary files a/Latest_Compiled_Version/Habbo-4.1.0-jar-with-dependencies.jar and b/Latest_Compiled_Version/Habbo-4.1.0-jar-with-dependencies.jar differ