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/rooms/BuildersClubRoomSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/BuildersClubRoomSupport.java
index 9d2ac676..c0227618 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/BuildersClubRoomSupport.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/BuildersClubRoomSupport.java
@@ -27,8 +27,8 @@ public class BuildersClubRoomSupport {
private static final Logger LOGGER = LoggerFactory.getLogger(BuildersClubRoomSupport.class);
public static final int DEFAULT_TRIAL_FURNI_LIMIT = 50;
- // Uses the built-in system account row so Builders Club furni have a valid foreign-key owner in `items`,
- // while still being treated as virtual / non-user-owned everywhere else in the BC flow.
+ // Runtime-only owner marker used to display Builders Club furni as virtual/non-user-owned in-room.
+ // The actual DB owner for persistence/FK purposes is tracked separately on the item instance.
public static final int VIRTUAL_OWNER_ID = 1;
public static final String DISPLAY_OWNER_NAME = "Builders Club";
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java
index e6f991f1..bc330fe1 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java
@@ -628,7 +628,7 @@ public class RoomItemManager {
}
if (BuildersClubRoomSupport.isTrackedItem(item.getId()) && item.getUserId() != BuildersClubRoomSupport.VIRTUAL_OWNER_ID) {
- item.setUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
+ item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
item.needsUpdate(true);
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboItem.java
index 9a8849fd..ec9ed53e 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboItem.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboItem.java
@@ -45,6 +45,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
private int id;
private int userId;
+ private int databaseUserId;
private int roomId;
private Item baseItem;
private String wallPosition;
@@ -62,6 +63,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
public HabboItem(ResultSet set, Item baseItem) throws SQLException {
this.id = set.getInt("id");
this.userId = set.getInt("user_id");
+ this.databaseUserId = this.userId;
this.roomId = set.getInt("room_id");
this.baseItem = baseItem;
this.wallPosition = set.getString("wall_pos");
@@ -81,6 +83,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
public HabboItem(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
this.id = id;
this.userId = userId;
+ this.databaseUserId = userId;
this.roomId = 0;
this.baseItem = item;
this.wallPosition = "";
@@ -169,6 +172,11 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
public void setUserId(int userId) {
this.userId = userId;
+ this.databaseUserId = userId;
+ }
+
+ public void setVirtualUserId(int userId) {
+ this.userId = userId;
}
public int getRoomId() {
@@ -275,7 +283,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
}
} else if (this.needsUpdate) {
try (PreparedStatement statement = connection.prepareStatement("UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?")) {
- statement.setInt(1, this.userId);
+ statement.setInt(1, this.databaseUserId);
statement.setInt(2, this.roomId);
statement.setString(3, this.wallPosition);
statement.setInt(4, this.x);
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
* 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: - *
- * - *- * WiredTickService (singleton) - * └── ScheduledExecutorService (50ms tick) - * └── For each room with tickables: - * └── For each WiredTickable: - * └── onWiredTick(room, currentTime) - *- * - *
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- * 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- * Should be called when a room is unloaded. - *
- * - * @param room the room - */ + public void unregisterRoom(Room room) { if (room == null) { return; } - + Set- * 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