From d52609e78bc1e9e3b56d7134ed7b095385808f7e Mon Sep 17 00:00:00 2001
From: duckietm
Date: Fri, 3 Apr 2026 14:09:25 +0200
Subject: [PATCH 01/11] =?UTF-8?q?=F0=9F=86=99=20Update=20Optimize=20script?=
=?UTF-8?q?,=20if=20run=20please=20run=20again=20!?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Database Updates/001_optimize_gameserver.sql | 1100 ++++++------------
1 file changed, 372 insertions(+), 728 deletions(-)
diff --git a/Database Updates/001_optimize_gameserver.sql b/Database Updates/001_optimize_gameserver.sql
index ffd8840a..5183aa52 100644
--- a/Database Updates/001_optimize_gameserver.sql
+++ b/Database Updates/001_optimize_gameserver.sql
@@ -141,245 +141,203 @@ ALTER TABLE IF EXISTS `guilds_forums_comments`
-- PHASE 3: Add missing primary keys
-- =============================================================================
-- Tables without primary keys hurt performance and replication.
+-- Uses a helper procedure to safely add `id` column only if it doesn't exist.
--- bot_serves: no PK
-ALTER TABLE IF EXISTS `bot_serves`
- ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST;
+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 ;
--- 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;
+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`('crafting_recipes_ingredients');
--- commandlogs: no PK
-ALTER TABLE IF EXISTS `commandlogs`
- ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST;
+-- items_hoppers: use existing item_id as PK (skip if PK already exists)
+SET @has_pk = (SELECT COUNT(*) FROM `information_schema`.`TABLE_CONSTRAINTS`
+ WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'items_hoppers' AND `CONSTRAINT_TYPE` = 'PRIMARY KEY');
+SET @sql = IF(@has_pk = 0, 'ALTER TABLE `items_hoppers` ADD PRIMARY KEY (`item_id`)', 'SELECT 1');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
--- crafting_recipes_ingredients: no PK
-ALTER TABLE IF EXISTS `crafting_recipes_ingredients`
- ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST;
+CALL `_add_id_pk_if_missing`('items_presents');
+CALL `_add_id_pk_if_missing`('items_teleports');
+CALL `_add_id_pk_if_missing`('namechange_log');
+CALL `_add_id_pk_if_missing`('navigator_publics');
--- items_hoppers: no PK
-ALTER TABLE IF EXISTS `items_hoppers`
- ADD PRIMARY KEY (`item_id`);
+CALL `_add_id_pk_if_missing`('pet_breeding');
+CALL `_add_id_pk_if_missing`('pet_breeding_races');
+CALL `_add_id_pk_if_missing`('pet_drinks');
+CALL `_add_id_pk_if_missing`('pet_foods');
+CALL `_add_id_pk_if_missing`('pet_items');
+CALL `_add_id_pk_if_missing`('pet_vocals');
+CALL `_add_id_pk_if_missing`('recycler_prizes');
+CALL `_add_id_pk_if_missing`('room_bans');
+CALL `_add_id_pk_if_missing`('room_enter_log');
+CALL `_add_id_pk_if_missing`('room_game_scores');
+CALL `_add_id_pk_if_missing`('room_mutes');
+CALL `_add_id_pk_if_missing`('room_rights');
+CALL `_add_id_pk_if_missing`('room_trax');
+CALL `_add_id_pk_if_missing`('room_trax_playlist');
+CALL `_add_id_pk_if_missing`('room_votes');
+CALL `_add_id_pk_if_missing`('trax_playlist');
+CALL `_add_id_pk_if_missing`('wired_rewards_given');
--- items_presents: no PK
-ALTER TABLE IF EXISTS `items_presents`
- ADD COLUMN `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST;
+CALL `_add_id_pk_if_missing`('calendar_rewards_claimed');
--- 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`);
+-- camera: ensure id is auto-increment PK (skip ADD PK if already exists)
+SET @has_pk = (SELECT COUNT(*) FROM `information_schema`.`TABLE_CONSTRAINTS`
+ WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'camera' AND `CONSTRAINT_TYPE` = 'PRIMARY KEY');
+SET @sql = IF(@has_pk = 0,
+ 'ALTER TABLE `camera` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, ADD PRIMARY KEY (`id`)',
+ 'ALTER TABLE `camera` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+-- Clean up helper procedure
+DROP PROCEDURE IF EXISTS `_add_id_pk_if_missing`;
-- =============================================================================
-- PHASE 4: Add missing indexes
-- =============================================================================
-- Adding indexes for columns commonly used in JOINs and WHERE clauses.
+-- Uses a helper procedure to skip indexes that already exist.
--- 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));
+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
+ -- Table does not exist, skip
+ SET @dummy = 0;
+ ELSE
+ 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 ;
--- calendar_rewards: index on campaign_id
-ALTER TABLE IF EXISTS `calendar_rewards`
- ADD INDEX `idx_calendar_rewards_campaign_id` (`campaign_id`);
+-- bans
+CALL `_add_index_if_missing`('bans', 'idx_bans_user_id', '`user_id`');
+CALL `_add_index_if_missing`('bans', 'idx_bans_ip', '`ip`');
+CALL `_add_index_if_missing`('bans', 'idx_bans_machine_id', '`machine_id`(64)');
--- 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`);
+-- calendar_rewards
+CALL `_add_index_if_missing`('calendar_rewards', 'idx_calendar_rewards_campaign_id', '`campaign_id`');
--- camera: indexes
-ALTER TABLE IF EXISTS `camera`
- ADD INDEX `idx_camera_user_id` (`user_id`),
- ADD INDEX `idx_camera_room_id` (`room_id`);
+-- calendar_rewards_claimed
+CALL `_add_index_if_missing`('calendar_rewards_claimed', 'idx_cal_claimed_user_id', '`user_id`');
+CALL `_add_index_if_missing`('calendar_rewards_claimed', 'idx_cal_claimed_campaign_id', '`campaign_id`');
+CALL `_add_index_if_missing`('calendar_rewards_claimed', 'idx_cal_claimed_reward_id', '`reward_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`);
+-- camera
+CALL `_add_index_if_missing`('camera', 'idx_camera_user_id', '`user_id`');
+CALL `_add_index_if_missing`('camera', 'idx_camera_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
+CALL `_add_index_if_missing`('guilds', 'idx_guilds_user_id', '`user_id`');
+CALL `_add_index_if_missing`('guilds', 'idx_guilds_room_id', '`room_id`');
--- guilds_forums_comments: index on user_id
-ALTER TABLE IF EXISTS `guilds_forums_comments`
- ADD INDEX `idx_gfc_user_id` (`user_id`);
+-- guilds_forums_threads
+CALL `_add_index_if_missing`('guilds_forums_threads', 'idx_gft_guild_id', '`guild_id`');
+CALL `_add_index_if_missing`('guilds_forums_threads', 'idx_gft_opener_id', '`opener_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`);
+-- guilds_forums_comments
+CALL `_add_index_if_missing`('guilds_forums_comments', 'idx_gfc_user_id', '`user_id`');
--- items_crackable: index on item_id
-ALTER TABLE IF EXISTS `items_crackable`
- ADD INDEX `idx_items_crackable_item_id` (`item_id`);
+-- guild_forum_views
+CALL `_add_index_if_missing`('guild_forum_views', 'idx_gfv_user_id', '`user_id`');
+CALL `_add_index_if_missing`('guild_forum_views', 'idx_gfv_guild_id', '`guild_id`');
--- namechange_log: index on user_id
-ALTER TABLE IF EXISTS `namechange_log`
- ADD INDEX `idx_namechange_user_id` (`user_id`);
+-- items_crackable
+CALL `_add_index_if_missing`('items_crackable', 'idx_items_crackable_item_id', '`item_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`);
+-- namechange_log
+CALL `_add_index_if_missing`('namechange_log', 'idx_namechange_user_id', '`user_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`);
+-- messenger_friendrequests
+CALL `_add_index_if_missing`('messenger_friendrequests', 'idx_fr_user_to_id', '`user_to_id`');
+CALL `_add_index_if_missing`('messenger_friendrequests', 'idx_fr_user_from_id', '`user_from_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_bans
+CALL `_add_index_if_missing`('room_bans', 'idx_room_bans_room_id', '`room_id`');
+CALL `_add_index_if_missing`('room_bans', 'idx_room_bans_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`);
+-- room_mutes
+CALL `_add_index_if_missing`('room_mutes', 'idx_room_mutes_room_id', '`room_id`');
+CALL `_add_index_if_missing`('room_mutes', 'idx_room_mutes_user_id', '`user_id`');
--- sanctions: index on habbo_id
-ALTER TABLE IF EXISTS `sanctions`
- ADD INDEX `idx_sanctions_habbo_id` (`habbo_id`);
+-- room_votes
+CALL `_add_index_if_missing`('room_votes', 'idx_room_votes_room_id', '`room_id`');
--- shadowbans: index on user_id
-ALTER TABLE IF EXISTS `shadowbans`
- ADD INDEX `idx_shadowbans_user_id` (`user_id`);
+-- sanctions
+CALL `_add_index_if_missing`('sanctions', 'idx_sanctions_habbo_id', '`habbo_id`');
--- support_cfh_topics: index on category_id
-ALTER TABLE IF EXISTS `support_cfh_topics`
- ADD INDEX `idx_cfh_topics_category_id` (`category_id`);
+-- shadowbans
+CALL `_add_index_if_missing`('shadowbans', 'idx_shadowbans_user_id', '`user_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`);
+-- support_cfh_topics
+CALL `_add_index_if_missing`('support_cfh_topics', 'idx_cfh_topics_category_id', '`category_id`');
--- users_settings: already has user_id index, good
+-- support_tickets
+CALL `_add_index_if_missing`('support_tickets', 'idx_tickets_sender_id', '`sender_id`');
+CALL `_add_index_if_missing`('support_tickets', 'idx_tickets_reported_id', '`reported_id`');
+CALL `_add_index_if_missing`('support_tickets', 'idx_tickets_mod_id', '`mod_id`');
+CALL `_add_index_if_missing`('support_tickets', 'idx_tickets_room_id', '`room_id`');
--- 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`);
+-- voucher_history
+CALL `_add_index_if_missing`('voucher_history', 'idx_vh_voucher_id', '`voucher_id`');
+CALL `_add_index_if_missing`('voucher_history', '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`);
+CALL `_add_index_if_missing`('ls_name_backgrounds_owned', 'idx_lsnbo_user_id', '`user_id`');
+CALL `_add_index_if_missing`('ls_name_backgrounds_owned', '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`);
+CALL `_add_index_if_missing`('ls_name_colors_owned', 'idx_lsnco_user_id', '`user_id`');
+CALL `_add_index_if_missing`('ls_name_colors_owned', '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`);
+CALL `_add_index_if_missing`('ls_prefixes_owned', 'idx_lspo_user_id', '`user_id`');
+CALL `_add_index_if_missing`('ls_prefixes_owned', '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`);
+CALL `_add_index_if_missing`('users_target_offer_purchases', 'idx_utop_offer_id', '`offer_id`');
-- users_unlockable_commands
-ALTER TABLE IF EXISTS `users_unlockable_commands`
- ADD INDEX `idx_uuc_user_id` (`user_id`);
+CALL `_add_index_if_missing`('users_unlockable_commands', '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`);
+-- command_category_permissions
+CALL `_add_index_if_missing`('command_category_permissions', 'idx_ccp_category_id', '`category_id`');
+
+-- Clean up helper procedure
+DROP PROCEDURE IF EXISTS `_add_index_if_missing`;
-- =============================================================================
@@ -397,584 +355,270 @@ ALTER TABLE IF EXISTS `command_category_permissions`
-- =============================================================================
-- ---------------------------------------------------------------------------
--- 5.1 Rooms → Users (owner)
+-- 5.0 Clean orphan rows BEFORE adding foreign keys
-- ---------------------------------------------------------------------------
-ALTER TABLE IF EXISTS `rooms`
- ADD CONSTRAINT `fk_rooms_owner`
- FOREIGN KEY (`owner_id`) REFERENCES `users` (`id`)
- ON DELETE CASCADE;
+-- Legacy Habbo databases often have rows referencing deleted users, rooms, or
+-- items. These orphans must be removed before FK constraints can be created.
+-- Uses a helper procedure to safely skip tables that don't exist.
+
+DELIMITER //
+DROP PROCEDURE IF EXISTS `_delete_orphans`//
+CREATE PROCEDURE `_delete_orphans`(IN tbl VARCHAR(64), IN col VARCHAR(64), IN ref_tbl VARCHAR(64), IN ref_col VARCHAR(64), IN extra_where VARCHAR(255))
+BEGIN
+ DECLARE tbl_exists INT DEFAULT 0;
+ DECLARE ref_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
+ IF extra_where IS NOT NULL AND extra_where != '' THEN
+ SET @sql = CONCAT('DELETE FROM `', tbl, '` WHERE ', extra_where, ' AND `', col, '` NOT IN (SELECT `', ref_col, '` FROM `', ref_tbl, '`)');
+ ELSE
+ SET @sql = CONCAT('DELETE FROM `', tbl, '` WHERE `', col, '` NOT IN (SELECT `', ref_col, '` FROM `', ref_tbl, '`)');
+ END IF;
+ PREPARE stmt FROM @sql;
+ EXECUTE stmt;
+ DEALLOCATE PREPARE stmt;
+ END IF;
+END//
+DELIMITER ;
+
+-- Orphans referencing users
+CALL `_delete_orphans`('rooms', 'owner_id', 'users', 'id', '');
+CALL `_delete_orphans`('items', 'user_id', 'users', 'id', '');
+-- bots: skipped, no FK added (see 5.3 comment)
+CALL `_delete_orphans`('bans', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('bans', 'user_staff_id', 'users', 'id', '');
+CALL `_delete_orphans`('guilds', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('guilds_members', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('guilds_forums_comments', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('guild_forum_views', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_settings', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_badges', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_currency', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_effects', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_clothing', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_favorite_rooms', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_wardrobe', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_pets', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_recipes', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_saved_searches', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_navigator_settings', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_achievements', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_achievements_queue', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_ignored', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_ignored', 'target_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_subscriptions', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_target_offer_purchases', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_unlockable_outfit', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_window_settings', 'user_id', 'users', 'id', '');
+CALL `_delete_orphans`('users_prefixes', 'user_id', 'users', 'id', '');
+
+-- Orphans referencing rooms
+CALL `_delete_orphans`('guilds', 'room_id', 'rooms', 'id', '');
+CALL `_delete_orphans`('items', 'room_id', 'rooms', 'id', '`room_id` != 0');
+
+-- Orphans referencing items_base
+CALL `_delete_orphans`('items', 'item_id', 'items_base', 'id', '');
+
+-- Orphans referencing guilds
+CALL `_delete_orphans`('guilds_members', 'guild_id', 'guilds', 'id', '');
+CALL `_delete_orphans`('guilds_forums_threads', 'guild_id', 'guilds', 'id', '');
+CALL `_delete_orphans`('guild_forum_views', 'guild_id', 'guilds', 'id', '');
+
+-- Clean up helper procedure
+DROP PROCEDURE IF EXISTS `_delete_orphans`;
-- ---------------------------------------------------------------------------
--- 5.2 Items → Users, Rooms, Items_base
+-- Helper: add FK only if it doesn't already exist
-- ---------------------------------------------------------------------------
-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;
+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 OR ref_exists = 0 THEN
+ -- Source or referenced table does not exist, skip
+ SET @dummy = 0;
+ ELSE
+ SELECT COUNT(*) INTO fk_exists
+ FROM `information_schema`.`TABLE_CONSTRAINTS`
+ WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = tbl AND `CONSTRAINT_NAME` = fk_name AND `CONSTRAINT_TYPE` = 'FOREIGN KEY';
+ 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 ;
--- 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.1 Rooms → Users
+CALL `_add_fk_if_missing`('rooms', 'fk_rooms_owner', 'owner_id', 'users', 'id', 'CASCADE');
+
+-- 5.2 Items → Users, Items_base
+CALL `_add_fk_if_missing`('items', 'fk_items_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('items', 'fk_items_item_base', 'item_id', 'items_base', 'id', 'CASCADE');
--- ---------------------------------------------------------------------------
-- 5.3 Bots → Users
--- ---------------------------------------------------------------------------
-ALTER TABLE IF EXISTS `bots`
- ADD CONSTRAINT `fk_bots_user`
- FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
- ON DELETE CASCADE;
+-- SKIPPED: The emulator creates bots with a temporary user_id during catalog
+-- purchase, which may not yet exist in `users`. Drop FK if previously added.
+SET @has_fk = (SELECT COUNT(*) FROM `information_schema`.`TABLE_CONSTRAINTS`
+ WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'bots' AND `CONSTRAINT_NAME` = 'fk_bots_user' AND `CONSTRAINT_TYPE` = 'FOREIGN KEY');
+SET @sql = IF(@has_fk > 0, 'ALTER TABLE `bots` DROP FOREIGN KEY `fk_bots_user`', 'SELECT 1');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
--- ---------------------------------------------------------------------------
-- 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;
+CALL `_add_fk_if_missing`('bans', 'fk_bans_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('bans', 'fk_bans_staff', 'user_staff_id', 'users', 'id', '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;
+CALL `_add_fk_if_missing`('guilds', 'fk_guilds_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('guilds', 'fk_guilds_room', 'room_id', 'rooms', 'id', '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;
+CALL `_add_fk_if_missing`('guilds_members', 'fk_gm_guild', 'guild_id', 'guilds', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('guilds_members', 'fk_gm_user', 'user_id', 'users', 'id', '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;
+CALL `_add_fk_if_missing`('guilds_forums_threads', 'fk_gft_guild', 'guild_id', 'guilds', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('guilds_forums_threads', 'fk_gft_opener', 'opener_id', 'users', 'id', '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;
+CALL `_add_fk_if_missing`('guilds_forums_comments', 'fk_gfc_thread', 'thread_id', 'guilds_forums_threads', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('guilds_forums_comments', 'fk_gfc_user', 'user_id', 'users', 'id', '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;
+CALL `_add_fk_if_missing`('guild_forum_views', 'fk_gfv_guild', 'guild_id', 'guilds', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('guild_forum_views', 'fk_gfv_user', 'user_id', 'users', 'id', '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;
+CALL `_add_fk_if_missing`('catalog_items', 'fk_catitems_page', 'page_id', 'catalog_pages', 'id', '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;
+CALL `_add_fk_if_missing`('catalog_items_limited', 'fk_catitemsltd_catitem', 'catalog_item_id', 'catalog_items', 'id', '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;
+CALL `_add_fk_if_missing`('calendar_rewards', 'fk_calrewards_campaign', 'campaign_id', 'calendar_campaigns', 'id', '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;
+CALL `_add_fk_if_missing`('calendar_rewards_claimed', 'fk_calclaimed_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('calendar_rewards_claimed', 'fk_calclaimed_campaign', 'campaign_id', 'calendar_campaigns', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('calendar_rewards_claimed', 'fk_calclaimed_reward', 'reward_id', 'calendar_rewards', 'id', '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.14-5.26 Users_* → Users
+CALL `_add_fk_if_missing`('users_settings', 'fk_usettings_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_badges', 'fk_ubadges_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_currency', 'fk_ucurrency_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_effects', 'fk_ueffects_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_clothing', 'fk_uclothing_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_favorite_rooms', 'fk_ufavrooms_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_favorite_rooms', 'fk_ufavrooms_room', 'room_id', 'rooms', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_wardrobe', 'fk_uwardrobe_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_pets', 'fk_upets_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_recipes', 'fk_urecipes_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_saved_searches', 'fk_usavedsearches_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_navigator_settings', 'fk_unavsettings_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_achievements', 'fk_uachievements_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_achievements_queue', 'fk_uachqueue_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_ignored', 'fk_uignored_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_ignored', 'fk_uignored_target', 'target_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_subscriptions', 'fk_usubs_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_target_offer_purchases', 'fk_utop_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_target_offer_purchases', 'fk_utop_offer', 'offer_id', 'catalog_target_offers', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('users_unlockable_commands', 'fk_uunlockable_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('user_window_settings', 'fk_uwinsettings_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('user_prefixes', 'fk_uprefixes_user', 'user_id', 'users', 'id', '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.33-5.35 Messenger → Users
+CALL `_add_fk_if_missing`('messenger_friendships', 'fk_mfriends_user_one', 'user_one_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('messenger_friendships', 'fk_mfriends_user_two', 'user_two_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('messenger_friendrequests', 'fk_mfr_user_to', 'user_to_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('messenger_friendrequests', 'fk_mfr_user_from', 'user_from_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('messenger_categories', 'fk_mcat_user', 'user_id', 'users', 'id', '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.36 Marketplace → Items, Users
+CALL `_add_fk_if_missing`('marketplace_items', 'fk_market_item', 'item_id', 'items', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('marketplace_items', 'fk_market_user', 'user_id', 'users', 'id', '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.37-5.44 Room_* → Rooms, Users
+CALL `_add_fk_if_missing`('room_rights', 'fk_rrights_room', 'room_id', 'rooms', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('room_rights', 'fk_rrights_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('room_bans', 'fk_rbans_room', 'room_id', 'rooms', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('room_bans', 'fk_rbans_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('room_mutes', 'fk_rmutes_room', 'room_id', 'rooms', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('room_mutes', 'fk_rmutes_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('room_votes', 'fk_rvotes_room', 'room_id', 'rooms', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('room_votes', 'fk_rvotes_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('room_wordfilter', 'fk_rwf_room', 'room_id', 'rooms', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('room_promotions', 'fk_rpromo_room', 'room_id', 'rooms', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('room_trax', 'fk_rtrax_room', 'room_id', 'rooms', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('room_trax_playlist', 'fk_rtraxpl_room', 'room_id', 'rooms', 'id', '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;
+CALL `_add_fk_if_missing`('rooms_for_sale', 'fk_r4sale_room', 'room_id', 'rooms', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('rooms_for_sale', 'fk_r4sale_user', 'user_id', 'users', 'id', '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.46-5.47 Room_trade_log → Users
+CALL `_add_fk_if_missing`('room_trade_log', 'fk_rtlog_user_one', 'user_one_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('room_trade_log', 'fk_rtlog_user_two', 'user_two_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('room_trade_log_items', 'fk_rtli_trade', 'id', 'room_trade_log', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('room_trade_log_items', 'fk_rtli_user', 'user_id', 'users', 'id', '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-5.49 Polls → Polls, Users
+CALL `_add_fk_if_missing`('polls_questions', 'fk_pq_poll', 'poll_id', 'polls', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('polls_answers', 'fk_pa_poll', 'poll_id', 'polls', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('polls_answers', 'fk_pa_user', 'user_id', 'users', 'id', '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.50-5.51 Crafting → Crafting_recipes
+CALL `_add_fk_if_missing`('crafting_recipes_ingredients', 'fk_cri_recipe', 'recipe_id', 'crafting_recipes', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('crafting_altars_recipes', 'fk_car_recipe', 'recipe_id', 'crafting_recipes', 'id', '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;
+CALL `_add_fk_if_missing`('voucher_history', 'fk_vh_voucher', 'voucher_id', 'vouchers', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('voucher_history', 'fk_vh_user', 'user_id', 'users', 'id', '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.53-5.55 Support, Commands, Economy
+CALL `_add_fk_if_missing`('support_cfh_topics', 'fk_cfhtopics_category', 'category_id', 'support_cfh_categories', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('command_category_permissions', 'fk_ccp_category', 'category_id', 'command_categories', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('economy_furniture', 'fk_econfurni_itembase', 'items_base_id', 'items_base', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('economy_furniture', 'fk_econfurni_category', 'economy_categories_id', 'economy_categories', 'id', '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.56-5.57 Sanctions, Camera → Users
+CALL `_add_fk_if_missing`('sanctions', 'fk_sanctions_user', 'habbo_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('camera_web', 'fk_cameraweb_user', 'user_id', 'users', 'id', '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.58 LS ownership → Users, LS definitions
+CALL `_add_fk_if_missing`('ls_name_backgrounds_owned', 'fk_lsnbo_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('ls_name_backgrounds_owned', 'fk_lsnbo_bg', 'name_background_id', 'ls_name_backgrounds', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('ls_name_colors_owned', 'fk_lsnco_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('ls_name_colors_owned', 'fk_lsnco_color', 'name_color_id', 'ls_name_colors', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('ls_prefixes_owned', 'fk_lspo_user', 'user_id', 'users', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('ls_prefixes_owned', 'fk_lspo_prefix', 'prefix_id', 'ls_prefixes', 'id', '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;
+CALL `_add_fk_if_missing`('navigator_publics', 'fk_navpub_cat', 'public_cat_id', 'navigator_publiccats', 'id', 'CASCADE');
+CALL `_add_fk_if_missing`('navigator_publics', 'fk_navpub_room', 'room_id', 'rooms', 'id', '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;
+CALL `_add_fk_if_missing`('gotw_winners', 'fk_gotw_user', 'user_id', 'users', 'id', 'CASCADE');
+
+-- Clean up helper procedure
+DROP PROCEDURE IF EXISTS `_add_fk_if_missing`;
-- =============================================================================
From c030ea5fc6d4ba9263728bd7bfa285e089e562fd Mon Sep 17 00:00:00 2001
From: duckietm
Date: Fri, 3 Apr 2026 19:10:55 +0200
Subject: [PATCH 02/11] =?UTF-8?q?=F0=9F=86=99=20Small=20update=20to=20SQL?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Database Updates/001_optimize_gameserver.sql | 615 +------------------
1 file changed, 20 insertions(+), 595 deletions(-)
diff --git a/Database Updates/001_optimize_gameserver.sql b/Database Updates/001_optimize_gameserver.sql
index 5183aa52..88546176 100644
--- a/Database Updates/001_optimize_gameserver.sql
+++ b/Database Updates/001_optimize_gameserver.sql
@@ -1,148 +1,20 @@
--- =============================================================================
--- 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;
--- =============================================================================
--- 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.
--- Uses a helper procedure to safely add `id` column only if it doesn't exist.
-
DELIMITER //
DROP PROCEDURE IF EXISTS `_add_id_pk_if_missing`//
CREATE PROCEDURE `_add_id_pk_if_missing`(IN tbl VARCHAR(64))
@@ -163,59 +35,7 @@ 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`('crafting_recipes_ingredients');
-
--- items_hoppers: use existing item_id as PK (skip if PK already exists)
-SET @has_pk = (SELECT COUNT(*) FROM `information_schema`.`TABLE_CONSTRAINTS`
- WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'items_hoppers' AND `CONSTRAINT_TYPE` = 'PRIMARY KEY');
-SET @sql = IF(@has_pk = 0, 'ALTER TABLE `items_hoppers` ADD PRIMARY KEY (`item_id`)', 'SELECT 1');
-PREPARE stmt FROM @sql;
-EXECUTE stmt;
-DEALLOCATE PREPARE stmt;
-
-CALL `_add_id_pk_if_missing`('items_presents');
-CALL `_add_id_pk_if_missing`('items_teleports');
-CALL `_add_id_pk_if_missing`('namechange_log');
-CALL `_add_id_pk_if_missing`('navigator_publics');
-
-CALL `_add_id_pk_if_missing`('pet_breeding');
-CALL `_add_id_pk_if_missing`('pet_breeding_races');
-CALL `_add_id_pk_if_missing`('pet_drinks');
-CALL `_add_id_pk_if_missing`('pet_foods');
-CALL `_add_id_pk_if_missing`('pet_items');
-CALL `_add_id_pk_if_missing`('pet_vocals');
-CALL `_add_id_pk_if_missing`('recycler_prizes');
-CALL `_add_id_pk_if_missing`('room_bans');
CALL `_add_id_pk_if_missing`('room_enter_log');
-CALL `_add_id_pk_if_missing`('room_game_scores');
-CALL `_add_id_pk_if_missing`('room_mutes');
-CALL `_add_id_pk_if_missing`('room_rights');
-CALL `_add_id_pk_if_missing`('room_trax');
-CALL `_add_id_pk_if_missing`('room_trax_playlist');
-CALL `_add_id_pk_if_missing`('room_votes');
-CALL `_add_id_pk_if_missing`('trax_playlist');
-CALL `_add_id_pk_if_missing`('wired_rewards_given');
-
-CALL `_add_id_pk_if_missing`('calendar_rewards_claimed');
-
--- camera: ensure id is auto-increment PK (skip ADD PK if already exists)
-SET @has_pk = (SELECT COUNT(*) FROM `information_schema`.`TABLE_CONSTRAINTS`
- WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'camera' AND `CONSTRAINT_TYPE` = 'PRIMARY KEY');
-SET @sql = IF(@has_pk = 0,
- 'ALTER TABLE `camera` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, ADD PRIMARY KEY (`id`)',
- 'ALTER TABLE `camera` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT');
-PREPARE stmt FROM @sql;
-EXECUTE stmt;
-DEALLOCATE PREPARE stmt;
-
--- Clean up helper procedure
-DROP PROCEDURE IF EXISTS `_add_id_pk_if_missing`;
-
--- =============================================================================
--- PHASE 4: Add missing indexes
--- =============================================================================
--- Adding indexes for columns commonly used in JOINs and WHERE clauses.
--- Uses a helper procedure to skip indexes that already exist.
DELIMITER //
DROP PROCEDURE IF EXISTS `_add_index_if_missing`//
@@ -223,218 +43,22 @@ CREATE PROCEDURE `_add_index_if_missing`(IN tbl VARCHAR(64), IN idx VARCHAR(64),
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
- -- Table does not exist, skip
- SET @dummy = 0;
- ELSE
- SELECT COUNT(*) INTO idx_exists
- FROM `information_schema`.`STATISTICS`
- WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = tbl AND `INDEX_NAME` = idx;
+ 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;
+ PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
END IF;
END IF;
END//
DELIMITER ;
--- bans
CALL `_add_index_if_missing`('bans', 'idx_bans_user_id', '`user_id`');
-CALL `_add_index_if_missing`('bans', 'idx_bans_ip', '`ip`');
-CALL `_add_index_if_missing`('bans', 'idx_bans_machine_id', '`machine_id`(64)');
-
--- calendar_rewards
-CALL `_add_index_if_missing`('calendar_rewards', 'idx_calendar_rewards_campaign_id', '`campaign_id`');
-
--- calendar_rewards_claimed
-CALL `_add_index_if_missing`('calendar_rewards_claimed', 'idx_cal_claimed_user_id', '`user_id`');
-CALL `_add_index_if_missing`('calendar_rewards_claimed', 'idx_cal_claimed_campaign_id', '`campaign_id`');
-CALL `_add_index_if_missing`('calendar_rewards_claimed', 'idx_cal_claimed_reward_id', '`reward_id`');
-
--- camera
-CALL `_add_index_if_missing`('camera', 'idx_camera_user_id', '`user_id`');
-CALL `_add_index_if_missing`('camera', 'idx_camera_room_id', '`room_id`');
-
--- guilds
CALL `_add_index_if_missing`('guilds', 'idx_guilds_user_id', '`user_id`');
-CALL `_add_index_if_missing`('guilds', 'idx_guilds_room_id', '`room_id`');
-
--- guilds_forums_threads
-CALL `_add_index_if_missing`('guilds_forums_threads', 'idx_gft_guild_id', '`guild_id`');
-CALL `_add_index_if_missing`('guilds_forums_threads', 'idx_gft_opener_id', '`opener_id`');
-
--- guilds_forums_comments
-CALL `_add_index_if_missing`('guilds_forums_comments', 'idx_gfc_user_id', '`user_id`');
-
--- guild_forum_views
-CALL `_add_index_if_missing`('guild_forum_views', 'idx_gfv_user_id', '`user_id`');
-CALL `_add_index_if_missing`('guild_forum_views', 'idx_gfv_guild_id', '`guild_id`');
-
--- items_crackable
-CALL `_add_index_if_missing`('items_crackable', 'idx_items_crackable_item_id', '`item_id`');
-
--- namechange_log
-CALL `_add_index_if_missing`('namechange_log', 'idx_namechange_user_id', '`user_id`');
-
--- messenger_friendrequests
-CALL `_add_index_if_missing`('messenger_friendrequests', 'idx_fr_user_to_id', '`user_to_id`');
-CALL `_add_index_if_missing`('messenger_friendrequests', 'idx_fr_user_from_id', '`user_from_id`');
-
--- room_bans
CALL `_add_index_if_missing`('room_bans', 'idx_room_bans_room_id', '`room_id`');
-CALL `_add_index_if_missing`('room_bans', 'idx_room_bans_user_id', '`user_id`');
-
--- room_mutes
-CALL `_add_index_if_missing`('room_mutes', 'idx_room_mutes_room_id', '`room_id`');
-CALL `_add_index_if_missing`('room_mutes', 'idx_room_mutes_user_id', '`user_id`');
-
--- room_votes
-CALL `_add_index_if_missing`('room_votes', 'idx_room_votes_room_id', '`room_id`');
-
--- sanctions
-CALL `_add_index_if_missing`('sanctions', 'idx_sanctions_habbo_id', '`habbo_id`');
-
--- shadowbans
-CALL `_add_index_if_missing`('shadowbans', 'idx_shadowbans_user_id', '`user_id`');
-
--- support_cfh_topics
-CALL `_add_index_if_missing`('support_cfh_topics', 'idx_cfh_topics_category_id', '`category_id`');
-
--- support_tickets
-CALL `_add_index_if_missing`('support_tickets', 'idx_tickets_sender_id', '`sender_id`');
-CALL `_add_index_if_missing`('support_tickets', 'idx_tickets_reported_id', '`reported_id`');
-CALL `_add_index_if_missing`('support_tickets', 'idx_tickets_mod_id', '`mod_id`');
-CALL `_add_index_if_missing`('support_tickets', 'idx_tickets_room_id', '`room_id`');
-
--- voucher_history
-CALL `_add_index_if_missing`('voucher_history', 'idx_vh_voucher_id', '`voucher_id`');
-CALL `_add_index_if_missing`('voucher_history', 'idx_vh_user_id', '`user_id`');
-
--- ls_name_backgrounds_owned
-CALL `_add_index_if_missing`('ls_name_backgrounds_owned', 'idx_lsnbo_user_id', '`user_id`');
-CALL `_add_index_if_missing`('ls_name_backgrounds_owned', 'idx_lsnbo_bg_id', '`name_background_id`');
-
--- ls_name_colors_owned
-CALL `_add_index_if_missing`('ls_name_colors_owned', 'idx_lsnco_user_id', '`user_id`');
-CALL `_add_index_if_missing`('ls_name_colors_owned', 'idx_lsnco_color_id', '`name_color_id`');
-
--- ls_prefixes_owned
-CALL `_add_index_if_missing`('ls_prefixes_owned', 'idx_lspo_user_id', '`user_id`');
-CALL `_add_index_if_missing`('ls_prefixes_owned', 'idx_lspo_prefix_id', '`prefix_id`');
-
--- users_target_offer_purchases
-CALL `_add_index_if_missing`('users_target_offer_purchases', 'idx_utop_offer_id', '`offer_id`');
-
--- users_unlockable_commands
-CALL `_add_index_if_missing`('users_unlockable_commands', 'idx_uuc_user_id', '`user_id`');
-
--- command_category_permissions
-CALL `_add_index_if_missing`('command_category_permissions', 'idx_ccp_category_id', '`category_id`');
-
--- Clean up helper procedure
-DROP PROCEDURE IF EXISTS `_add_index_if_missing`;
--- =============================================================================
--- 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.0 Clean orphan rows BEFORE adding foreign keys
--- ---------------------------------------------------------------------------
--- Legacy Habbo databases often have rows referencing deleted users, rooms, or
--- items. These orphans must be removed before FK constraints can be created.
--- Uses a helper procedure to safely skip tables that don't exist.
-
-DELIMITER //
-DROP PROCEDURE IF EXISTS `_delete_orphans`//
-CREATE PROCEDURE `_delete_orphans`(IN tbl VARCHAR(64), IN col VARCHAR(64), IN ref_tbl VARCHAR(64), IN ref_col VARCHAR(64), IN extra_where VARCHAR(255))
-BEGIN
- DECLARE tbl_exists INT DEFAULT 0;
- DECLARE ref_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
- IF extra_where IS NOT NULL AND extra_where != '' THEN
- SET @sql = CONCAT('DELETE FROM `', tbl, '` WHERE ', extra_where, ' AND `', col, '` NOT IN (SELECT `', ref_col, '` FROM `', ref_tbl, '`)');
- ELSE
- SET @sql = CONCAT('DELETE FROM `', tbl, '` WHERE `', col, '` NOT IN (SELECT `', ref_col, '` FROM `', ref_tbl, '`)');
- END IF;
- PREPARE stmt FROM @sql;
- EXECUTE stmt;
- DEALLOCATE PREPARE stmt;
- END IF;
-END//
-DELIMITER ;
-
--- Orphans referencing users
-CALL `_delete_orphans`('rooms', 'owner_id', 'users', 'id', '');
-CALL `_delete_orphans`('items', 'user_id', 'users', 'id', '');
--- bots: skipped, no FK added (see 5.3 comment)
-CALL `_delete_orphans`('bans', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('bans', 'user_staff_id', 'users', 'id', '');
-CALL `_delete_orphans`('guilds', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('guilds_members', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('guilds_forums_comments', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('guild_forum_views', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_settings', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_badges', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_currency', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_effects', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_clothing', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_favorite_rooms', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_wardrobe', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_pets', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_recipes', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_saved_searches', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_navigator_settings', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_achievements', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_achievements_queue', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_ignored', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_ignored', 'target_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_subscriptions', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_target_offer_purchases', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_unlockable_outfit', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_window_settings', 'user_id', 'users', 'id', '');
-CALL `_delete_orphans`('users_prefixes', 'user_id', 'users', 'id', '');
-
--- Orphans referencing rooms
-CALL `_delete_orphans`('guilds', 'room_id', 'rooms', 'id', '');
-CALL `_delete_orphans`('items', 'room_id', 'rooms', 'id', '`room_id` != 0');
-
--- Orphans referencing items_base
-CALL `_delete_orphans`('items', 'item_id', 'items_base', 'id', '');
-
--- Orphans referencing guilds
-CALL `_delete_orphans`('guilds_members', 'guild_id', 'guilds', 'id', '');
-CALL `_delete_orphans`('guilds_forums_threads', 'guild_id', 'guilds', 'id', '');
-CALL `_delete_orphans`('guild_forum_views', 'guild_id', 'guilds', 'id', '');
-
--- Clean up helper procedure
-DROP PROCEDURE IF EXISTS `_delete_orphans`;
-
--- ---------------------------------------------------------------------------
--- Helper: add FK only if it doesn't already exist
--- ---------------------------------------------------------------------------
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))
@@ -442,226 +66,27 @@ 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 OR ref_exists = 0 THEN
- -- Source or referenced table does not exist, skip
- SET @dummy = 0;
- ELSE
- SELECT COUNT(*) INTO fk_exists
- FROM `information_schema`.`TABLE_CONSTRAINTS`
- WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = tbl AND `CONSTRAINT_NAME` = fk_name AND `CONSTRAINT_TYPE` = 'FOREIGN KEY';
+ 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;
+ PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
END IF;
END IF;
END//
DELIMITER ;
--- 5.1 Rooms → Users
CALL `_add_fk_if_missing`('rooms', 'fk_rooms_owner', 'owner_id', 'users', 'id', 'CASCADE');
-
--- 5.2 Items → Users, Items_base
CALL `_add_fk_if_missing`('items', 'fk_items_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('items', 'fk_items_item_base', 'item_id', 'items_base', 'id', 'CASCADE');
-
--- 5.3 Bots → Users
--- SKIPPED: The emulator creates bots with a temporary user_id during catalog
--- purchase, which may not yet exist in `users`. Drop FK if previously added.
-SET @has_fk = (SELECT COUNT(*) FROM `information_schema`.`TABLE_CONSTRAINTS`
- WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'bots' AND `CONSTRAINT_NAME` = 'fk_bots_user' AND `CONSTRAINT_TYPE` = 'FOREIGN KEY');
-SET @sql = IF(@has_fk > 0, 'ALTER TABLE `bots` DROP FOREIGN KEY `fk_bots_user`', 'SELECT 1');
-PREPARE stmt FROM @sql;
-EXECUTE stmt;
-DEALLOCATE PREPARE stmt;
-
--- 5.4 Bans → Users
-CALL `_add_fk_if_missing`('bans', 'fk_bans_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('bans', 'fk_bans_staff', 'user_staff_id', 'users', 'id', 'CASCADE');
-
--- 5.5 Guilds → Users, Rooms
-CALL `_add_fk_if_missing`('guilds', 'fk_guilds_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('guilds', 'fk_guilds_room', 'room_id', 'rooms', 'id', 'CASCADE');
-
--- 5.6 Guild members → Guilds, Users
-CALL `_add_fk_if_missing`('guilds_members', 'fk_gm_guild', 'guild_id', 'guilds', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('guilds_members', 'fk_gm_user', 'user_id', 'users', 'id', 'CASCADE');
-
--- 5.7 Guild forum threads → Guilds, Users
-CALL `_add_fk_if_missing`('guilds_forums_threads', 'fk_gft_guild', 'guild_id', 'guilds', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('guilds_forums_threads', 'fk_gft_opener', 'opener_id', 'users', 'id', 'SET NULL');
-
--- 5.8 Guild forum comments → Threads, Users
-CALL `_add_fk_if_missing`('guilds_forums_comments', 'fk_gfc_thread', 'thread_id', 'guilds_forums_threads', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('guilds_forums_comments', 'fk_gfc_user', 'user_id', 'users', 'id', 'CASCADE');
-
--- 5.9 Guild forum views → Guilds, Users
-CALL `_add_fk_if_missing`('guild_forum_views', 'fk_gfv_guild', 'guild_id', 'guilds', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('guild_forum_views', 'fk_gfv_user', 'user_id', 'users', 'id', 'CASCADE');
-
--- 5.10 Catalog items → Catalog pages
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');
--- 5.11 Catalog items limited → Catalog items
-CALL `_add_fk_if_missing`('catalog_items_limited', 'fk_catitemsltd_catitem', 'catalog_item_id', 'catalog_items', 'id', 'CASCADE');
-
--- 5.12 Calendar rewards → Calendar campaigns
-CALL `_add_fk_if_missing`('calendar_rewards', 'fk_calrewards_campaign', 'campaign_id', 'calendar_campaigns', 'id', 'CASCADE');
-
--- 5.13 Calendar rewards claimed → Users, Campaigns, Rewards
-CALL `_add_fk_if_missing`('calendar_rewards_claimed', 'fk_calclaimed_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('calendar_rewards_claimed', 'fk_calclaimed_campaign', 'campaign_id', 'calendar_campaigns', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('calendar_rewards_claimed', 'fk_calclaimed_reward', 'reward_id', 'calendar_rewards', 'id', 'CASCADE');
-
--- 5.14-5.26 Users_* → Users
-CALL `_add_fk_if_missing`('users_settings', 'fk_usettings_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_badges', 'fk_ubadges_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_currency', 'fk_ucurrency_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_effects', 'fk_ueffects_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_clothing', 'fk_uclothing_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_favorite_rooms', 'fk_ufavrooms_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_favorite_rooms', 'fk_ufavrooms_room', 'room_id', 'rooms', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_wardrobe', 'fk_uwardrobe_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_pets', 'fk_upets_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_recipes', 'fk_urecipes_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_saved_searches', 'fk_usavedsearches_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_navigator_settings', 'fk_unavsettings_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_achievements', 'fk_uachievements_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_achievements_queue', 'fk_uachqueue_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_ignored', 'fk_uignored_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_ignored', 'fk_uignored_target', 'target_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_subscriptions', 'fk_usubs_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_target_offer_purchases', 'fk_utop_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_target_offer_purchases', 'fk_utop_offer', 'offer_id', 'catalog_target_offers', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('users_unlockable_commands', 'fk_uunlockable_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('user_window_settings', 'fk_uwinsettings_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('user_prefixes', 'fk_uprefixes_user', 'user_id', 'users', 'id', 'CASCADE');
-
--- 5.33-5.35 Messenger → Users
-CALL `_add_fk_if_missing`('messenger_friendships', 'fk_mfriends_user_one', 'user_one_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('messenger_friendships', 'fk_mfriends_user_two', 'user_two_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('messenger_friendrequests', 'fk_mfr_user_to', 'user_to_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('messenger_friendrequests', 'fk_mfr_user_from', 'user_from_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('messenger_categories', 'fk_mcat_user', 'user_id', 'users', 'id', 'CASCADE');
-
--- 5.36 Marketplace → Items, Users
-CALL `_add_fk_if_missing`('marketplace_items', 'fk_market_item', 'item_id', 'items', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('marketplace_items', 'fk_market_user', 'user_id', 'users', 'id', 'CASCADE');
-
--- 5.37-5.44 Room_* → Rooms, Users
-CALL `_add_fk_if_missing`('room_rights', 'fk_rrights_room', 'room_id', 'rooms', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('room_rights', 'fk_rrights_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('room_bans', 'fk_rbans_room', 'room_id', 'rooms', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('room_bans', 'fk_rbans_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('room_mutes', 'fk_rmutes_room', 'room_id', 'rooms', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('room_mutes', 'fk_rmutes_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('room_votes', 'fk_rvotes_room', 'room_id', 'rooms', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('room_votes', 'fk_rvotes_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('room_wordfilter', 'fk_rwf_room', 'room_id', 'rooms', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('room_promotions', 'fk_rpromo_room', 'room_id', 'rooms', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('room_trax', 'fk_rtrax_room', 'room_id', 'rooms', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('room_trax_playlist', 'fk_rtraxpl_room', 'room_id', 'rooms', 'id', 'CASCADE');
-
--- 5.45 Rooms_for_sale → Rooms, Users
-CALL `_add_fk_if_missing`('rooms_for_sale', 'fk_r4sale_room', 'room_id', 'rooms', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('rooms_for_sale', 'fk_r4sale_user', 'user_id', 'users', 'id', 'CASCADE');
-
--- 5.46-5.47 Room_trade_log → Users
-CALL `_add_fk_if_missing`('room_trade_log', 'fk_rtlog_user_one', 'user_one_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('room_trade_log', 'fk_rtlog_user_two', 'user_two_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('room_trade_log_items', 'fk_rtli_trade', 'id', 'room_trade_log', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('room_trade_log_items', 'fk_rtli_user', 'user_id', 'users', 'id', 'CASCADE');
-
--- 5.48-5.49 Polls → Polls, Users
-CALL `_add_fk_if_missing`('polls_questions', 'fk_pq_poll', 'poll_id', 'polls', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('polls_answers', 'fk_pa_poll', 'poll_id', 'polls', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('polls_answers', 'fk_pa_user', 'user_id', 'users', 'id', 'CASCADE');
-
--- 5.50-5.51 Crafting → Crafting_recipes
-CALL `_add_fk_if_missing`('crafting_recipes_ingredients', 'fk_cri_recipe', 'recipe_id', 'crafting_recipes', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('crafting_altars_recipes', 'fk_car_recipe', 'recipe_id', 'crafting_recipes', 'id', 'CASCADE');
-
--- 5.52 Voucher_history → Vouchers, Users
-CALL `_add_fk_if_missing`('voucher_history', 'fk_vh_voucher', 'voucher_id', 'vouchers', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('voucher_history', 'fk_vh_user', 'user_id', 'users', 'id', 'CASCADE');
-
--- 5.53-5.55 Support, Commands, Economy
-CALL `_add_fk_if_missing`('support_cfh_topics', 'fk_cfhtopics_category', 'category_id', 'support_cfh_categories', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('command_category_permissions', 'fk_ccp_category', 'category_id', 'command_categories', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('economy_furniture', 'fk_econfurni_itembase', 'items_base_id', 'items_base', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('economy_furniture', 'fk_econfurni_category', 'economy_categories_id', 'economy_categories', 'id', 'CASCADE');
-
--- 5.56-5.57 Sanctions, Camera → Users
-CALL `_add_fk_if_missing`('sanctions', 'fk_sanctions_user', 'habbo_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('camera_web', 'fk_cameraweb_user', 'user_id', 'users', 'id', 'CASCADE');
-
--- 5.58 LS ownership → Users, LS definitions
-CALL `_add_fk_if_missing`('ls_name_backgrounds_owned', 'fk_lsnbo_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('ls_name_backgrounds_owned', 'fk_lsnbo_bg', 'name_background_id', 'ls_name_backgrounds', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('ls_name_colors_owned', 'fk_lsnco_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('ls_name_colors_owned', 'fk_lsnco_color', 'name_color_id', 'ls_name_colors', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('ls_prefixes_owned', 'fk_lspo_user', 'user_id', 'users', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('ls_prefixes_owned', 'fk_lspo_prefix', 'prefix_id', 'ls_prefixes', 'id', 'CASCADE');
-
--- 5.59 Navigator_publics → Navigator_publiccats, Rooms
-CALL `_add_fk_if_missing`('navigator_publics', 'fk_navpub_cat', 'public_cat_id', 'navigator_publiccats', 'id', 'CASCADE');
-CALL `_add_fk_if_missing`('navigator_publics', 'fk_navpub_room', 'room_id', 'rooms', 'id', 'CASCADE');
-
--- 5.60 GOTW winners → Users
-CALL `_add_fk_if_missing`('gotw_winners', 'fk_gotw_user', 'user_id', 'users', 'id', 'CASCADE');
-
--- Clean up helper procedure
+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 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
From 0916ff1e0bb11efe4f8df091a434adb60a3dd57e Mon Sep 17 00:00:00 2001
From: DuckieTM
Date: Sat, 4 Apr 2026 08:49:19 +0200
Subject: [PATCH 03/11] =?UTF-8?q?=F0=9F=86=99=20Redone=20the=20userwalk=20?=
=?UTF-8?q?flood=20detection?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../rooms/users/RoomUserWalkEvent.java | 48 +++++++++++++++++--
1 file changed, 45 insertions(+), 3 deletions(-)
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 e54fc6e7..46d385b4 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) {
From e6f824aedd34ee413a3e16eef1db8f7c43d3d436 Mon Sep 17 00:00:00 2001
From: DuckieTM
Date: Sat, 4 Apr 2026 16:19:46 +0200
Subject: [PATCH 04/11] =?UTF-8?q?=F0=9F=86=99=20Wired=20Core=20updates?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Thanks to Migueg
---
Database Updates/005_WiredTickService.sql | 1 +
.../habbohotel/wired/core/WiredEngine.java | 367 +++++++----
.../habbohotel/wired/core/WiredManager.java | 181 +++--
.../wired/tick/WiredTickService.java | 623 ++++++++++--------
.../Habbo-4.1.0-jar-with-dependencies.jar | Bin 21338433 -> 21342517 bytes
5 files changed, 695 insertions(+), 477 deletions(-)
create mode 100644 Database Updates/005_WiredTickService.sql
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/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 db624a5a..c2f4d1f1 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
@@ -39,7 +39,7 @@ import java.util.concurrent.ConcurrentHashMap;
* It receives {@link WiredEvent} objects, finds matching stacks via {@link WiredStackIndex},
* evaluates conditions, and executes effects.
*
- *
+ *
* Execution Flow:
*
* - Receive event via {@link #handleEvent(WiredEvent)}
@@ -49,14 +49,14 @@ import java.util.concurrent.ConcurrentHashMap;
* - Execute effects (respecting random/unseen modifiers)
* - Handle delays for timed effects
*
- *
+ *
* Safety Features:
*
* - Step limits via {@link WiredState} prevent infinite loops
* - Effect cooldowns prevent rapid re-triggering
* - Exceptions are caught and logged, not propagated
*
- *
+ *
* @see WiredEvent
* @see WiredContext
* @see WiredStackIndex
@@ -64,38 +64,41 @@ 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;
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;
+ /** 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)
@@ -104,7 +107,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;
@@ -112,11 +115,12 @@ public final class WiredEngine {
this.roomRecursionDepth = new ConcurrentHashMap<>();
this.eventRateLimiters = new ConcurrentHashMap<>();
this.bannedRooms = 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)
*/
@@ -129,20 +133,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) {
@@ -152,7 +150,7 @@ public final class WiredEngine {
return false;
}
roomRecursionDepth.put(roomId, currentDepth + 1);
-
+
try {
return handleEventInternal(event, room);
} finally {
@@ -165,7 +163,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.
*/
@@ -232,12 +352,12 @@ public final class WiredEngine {
// Initial step for trigger
state.step();
-
- debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})",
- event.getType(),
- stack.triggerItem() != null ? stack.triggerItem().getId() : "null",
- stack.conditions().size(),
- stack.effects().size());
+
+ debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})",
+ event.getType(),
+ stack.triggerItem() != null ? stack.triggerItem().getId() : "null",
+ stack.conditions().size(),
+ stack.effects().size());
// Run selectors before conditions so targets are available
List executedSelectors = Collections.emptyList();
@@ -409,11 +529,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) {
@@ -484,7 +604,7 @@ public final class WiredEngine {
ctx.state().step();
try {
effect.execute(ctx);
-
+
// Activate box animation after execution
if (effect instanceof InteractionWiredEffect) {
InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect;
@@ -599,7 +719,7 @@ public final class WiredEngine {
Collections.shuffle(result, Emulator.getRandom());
return new ArrayList<>(result.subList(0, limit));
}
-
+
/**
* Schedule a delayed effect execution.
*/
@@ -610,15 +730,15 @@ 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()) {
return;
}
-
+
try {
effect.execute(ctx);
-
+
// Activate box animation after execution
if (effect instanceof InteractionWiredEffect) {
InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect;
@@ -753,14 +873,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;
}
@@ -773,7 +893,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) {
@@ -785,7 +905,7 @@ public final class WiredEngine {
legacyConditions.add((InteractionWiredCondition) cond);
}
}
-
+
WiredStackTriggeredEvent triggeredEvent = new WiredStackTriggeredEvent(
event.getRoom(),
event.getActor().orElse(null),
@@ -793,7 +913,7 @@ public final class WiredEngine {
legacyEffects,
legacyConditions
);
-
+
return !Emulator.getPluginManager().fireEvent(triggeredEvent).isCancelled();
}
return true;
@@ -806,7 +926,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);
@@ -817,7 +937,7 @@ public final class WiredEngine {
legacyConditions.add((InteractionWiredCondition) cond);
}
}
-
+
Emulator.getPluginManager().fireEvent(new WiredStackExecutedEvent(
event.getRoom(),
event.getActor().orElse(null),
@@ -832,10 +952,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);
}
/**
@@ -845,10 +971,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);
@@ -926,7 +1052,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.
@@ -935,14 +1061,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
@@ -951,7 +1077,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.
@@ -961,83 +1087,81 @@ public final class WiredEngine {
String prefix = roomId + ":";
eventRateLimiters.keySet().removeIf(key -> key.startsWith(prefix));
}
-
+
+ /**
+ * Clear cached source-stack lookups for a specific room.
+ * @param roomId the room ID
+ */
+ public void clearRoomSourceStackCache(int roomId) {
+ String prefix = roomId + ":";
+ sourceStacksByTriggerKey.keySet().removeIf(key -> key.startsWith(prefix));
+ }
+
+ /**
+ * Clear all cached source-stack lookups.
+ */
+ public void clearAllSourceStackCache() {
+ sourceStacksByTriggerKey.clear();
+ }
+
+ /**
+ * Clear all execution-related caches for a specific room.
+ * @param roomId the room ID
+ */
+ public void clearRoomExecutionCaches(int roomId) {
+ clearRoomRecursionDepth(roomId);
+ clearRoomRateLimiters(roomId);
+ clearRoomSourceStackCache(roomId);
+ }
+
+ /**
+ * Clear all execution-related caches.
+ */
+ public void clearAllExecutionCaches() {
+ clearAllRecursionDepth();
+ eventRateLimiters.clear();
+ clearAllSourceStackCache();
+ clearUnseenCache();
+ }
+
/**
* Clear room ban for a specific room.
- * Should be called when a room is unloaded.
* @param roomId the room ID
*/
public void clearRoomBan(int roomId) {
- bannedRooms.remove(roomId);
+ // no-op
}
-
+
/**
* Check if a room is currently banned from wired execution.
* @param roomId the room ID
* @return true if wired is banned in this room
*/
private boolean isRoomBanned(int roomId) {
- Long banExpiry = bannedRooms.get(roomId);
- if (banExpiry == null) {
- return false;
- }
-
- if (System.currentTimeMillis() >= banExpiry) {
- // Ban expired, remove it
- bannedRooms.remove(roomId);
- return false;
- }
-
- return true;
+ return false;
}
-
+
/**
- * 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) {
- long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS;
- bannedRooms.put(roomId, banExpiry);
-
- long banMinutes = WIRED_BAN_DURATION_MS / 60000;
-
- // Send alert to all users in the room
- String roomAlertMessage = Emulator.getTexts().getValue("wired.abuse.room.alert")
- .replace("%minutes%", String.valueOf(banMinutes));
- room.sendComposer(new GenericAlertComposer(roomAlertMessage).compose());
-
- // Send scripter bubble alert to staff with room link
- THashMap keys = new THashMap<>();
- keys.put("title", Emulator.getTexts().getValue("wired.abuse.staff.title"));
- keys.put("message", Emulator.getTexts().getValue("wired.abuse.staff.message")
- .replace("%roomname%", room.getName())
- .replace("%owner%", room.getOwnerName())
- .replace("%minutes%", String.valueOf(banMinutes)));
- keys.put("linkUrl", "event:navigator/goto/" + roomId);
- keys.put("linkTitle", Emulator.getTexts().getValue("wired.abuse.staff.link"));
- Emulator.getGameEnvironment().getHabboManager().sendPacketToHabbosWithPermission(
- new BubbleAlertComposer("admin.staffalert", keys).compose(),
- "acc_modtool_room_info"
- );
-
- LOGGER.warn("Wired abuse detected in room {} ({}). Owner: {}. Wired banned for {} minutes.",
- roomId, room.getName(), room.getOwnerName(), banMinutes);
+ // no-op
}
-
+
/**
* 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);
@@ -1045,51 +1169,46 @@ public final class WiredEngine {
existing.recordEvent(now);
return existing;
});
-
+
boolean limited = tracker.isRateLimited(now);
if (limited && tracker.shouldBan(now)) {
- // First time hitting limit in this suppression window - ban the room
- banRoom(roomId, room);
+ LOGGER.warn("Soft wired rate limit in room {} for event {}. Count in current window exceeded.",
+ roomId, eventType);
}
return limited;
}
-
+
/**
* 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 a3077406..c7d0fd91 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
@@ -46,7 +46,7 @@ import java.sql.SQLException;
* wired engine. It provides static methods for triggering events and manages
* the lifecycle of the engine.
*
- *
+ *
* Configuration Options:
*
* - {@code wired.engine.enabled} - Enable new engine (parallel mode)
@@ -54,7 +54,7 @@ import java.sql.SQLException;
* - {@code wired.engine.maxStepsPerStack} - Loop protection limit
* - {@code wired.engine.debug} - Verbose logging
*
- *
+ *
* Migration Strategy:
*
* - Set {@code wired.engine.enabled=true} to run both engines in parallel
@@ -62,7 +62,7 @@ import java.sql.SQLException;
* - Set {@code wired.engine.exclusive=true} to disable legacy engine
* - Full migration complete - WiredManager is now the only wired engine
*
- *
+ *
* @see WiredEngine
* @see WiredEvents
*/
@@ -86,10 +86,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 WiredManager() {
@@ -119,7 +119,7 @@ public final class WiredManager {
boolean enabled = Emulator.getConfig().getBoolean(CONFIG_ENABLED, DEFAULT_ENABLED);
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);
@@ -133,13 +133,13 @@ public final class WiredManager {
stackIndex = new RoomWiredStackIndex();
WiredServices services = DefaultWiredServices.getInstance();
engine = new WiredEngine(services, stackIndex, maxSteps);
-
+
// Start the centralized tick service (50ms interval)
WiredTickService.getInstance().start();
initialized = true;
-
- LOGGER.info("Wired Manager initialized - enabled: {}, maxSteps: {}, debug: {}",
+
+ LOGGER.info("Wired Manager initialized - enabled: {}, maxSteps: {}, debug: {}",
enabled, maxSteps, debug);
}
@@ -153,16 +153,16 @@ 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.clearAllExecutionCaches();
}
initialized = false;
@@ -212,10 +212,22 @@ public final class WiredManager {
if (!isEnabled() || engine == null) {
return false;
}
-
+
return engine.handleEvent(event);
}
+ /**
+ * 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.
*/
@@ -223,7 +235,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);
}
@@ -235,7 +247,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);
}
@@ -311,7 +323,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.userSays(room, user, message);
return handleEvent(event);
}
@@ -332,7 +344,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.userEntersRoom(room, user);
return handleEvent(event);
}
@@ -356,7 +368,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || item == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.furniStateChanged(room, user, item);
return handleEvent(event);
}
@@ -365,24 +377,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) {
@@ -391,31 +403,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);
}
/**
@@ -425,7 +437,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.gameStarts(room);
return handleEvent(event);
}
@@ -437,7 +449,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.gameEnds(room);
return handleEvent(event);
}
@@ -449,7 +461,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || botUnit == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.botCollision(room, botUnit);
return handleEvent(event);
}
@@ -461,7 +473,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || botUnit == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.botReachedFurni(room, botUnit, item);
return handleEvent(event);
}
@@ -473,7 +485,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || botUnit == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.botReachedHabbo(room, botUnit, targetUser);
return handleEvent(event);
}
@@ -489,7 +501,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.scoreAchieved(room, user, score, scoreAdded);
return handleEvent(event);
}
@@ -501,7 +513,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.userIdles(room, user);
return handleEvent(event);
}
@@ -513,7 +525,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.userUnidles(room, user);
return handleEvent(event);
}
@@ -525,7 +537,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.userStartsDancing(room, user);
return handleEvent(event);
}
@@ -537,7 +549,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.userStopsDancing(room, user);
return handleEvent(event);
}
@@ -549,7 +561,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.teamWins(room, user);
return handleEvent(event);
}
@@ -561,7 +573,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.teamLoses(room, user);
return handleEvent(event);
}
@@ -574,7 +586,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
-
+
WiredEvent event = WiredEvents.fromLegacy(triggerType, room, roomUnit, stuff);
return handleEvent(event);
}
@@ -586,11 +598,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());
}
}
@@ -601,13 +622,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);
}
}
@@ -616,19 +649,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) {
@@ -637,19 +670,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
*/
@@ -660,7 +693,7 @@ public final class WiredManager {
}
// ========== JSON Utilities ==========
-
+
private static GsonBuilder gsonBuilder = null;
private static Gson cachedGson = null;
@@ -670,12 +703,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() {
@@ -686,50 +719,53 @@ 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 (engine != null && room != null) {
+ engine.clearRoomExecutionCaches(room.getId());
+ }
}
-
+
/**
* Gets the tick service instance.
- *
+ *
* @return the WiredTickService
*/
public static WiredTickService getTickService() {
@@ -771,7 +807,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) {
@@ -804,9 +840,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);
@@ -823,12 +859,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();
@@ -1066,4 +1102,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/Latest_Compiled_Version/Habbo-4.1.0-jar-with-dependencies.jar b/Latest_Compiled_Version/Habbo-4.1.0-jar-with-dependencies.jar
index 94c084b2adc8ab0d9d16745420ea3f1b431056c1..ed4de64618957a779ce31248a46edde8894999d9 100644
GIT binary patch
delta 497077
zcmZ^~1z1$w+W5^#NOwzjcc*knmvl;(NGdUupwbLoLwARSbV|1%AxJ3Qhz_XV9`Kyw
z^Ss~R>-w>0?sc!bR_q#k>Qm60W>V0xv>qWMp(7xmA|NP-h9_V#paKU|n>Pc538K7a
zd@X``k9x}Df95}?6C~Hbx;VbhDWH%x%``G{;`rDbEL=rwB=Wk?=$MFr
zN5~N-C;Zqzg*6?B$Q2L(IeSBgBnyNB3C1CWa6}Ox$-%$i;u1o*2GAguQ8d^}$`71fNa&<0MYIh3^|6gL-Xu&(<4*}zGxoFUuDj5(OBsd!V
zPCP|O2w99K{8ukfDHWtP4g-2pBLl*Mj8&rF#)A?gf(SATgvi85+Kwx?3i1I*WRynLtOEq2)KE{6r11^oLyw!qs0xj4Ot!mU;
zU6~j*NOvs$t$E-x)KZ-ngbrEx(>j&SnxnxBLdsmP1)=^cdFW(;QJsjpYbi_qBY{p)$%0Tw|B>Zi^9~A<
zc>ow(Ph^7DGRT53Aybe$1D(xx&N%uV20l|LgUQf%RV3r
z_0|P^7T0FsA}aiu(NDAgt`~g%!}pq68X^#0P8K0B_r$W%Zs2o+LMb=T3QnK|c`95B;={t?u)zIg`gZZ+V3EQ`qh^BEdnD+j`btkmCW_7o}Kw!kZ|HBJpI`1Mq3
zAnsocckMuLC`~8ac6jmKeEXkf@Ft-7ugKw^*Yoh9TBGnj(T(Vc@LbaynW2^#G$0Dd
z3Lt^!1C$hcl1&5Dw5sI4F=zA8f++rF5LEi`wgae?Cuf`%gaUa{dZ!Q1oTr==8owj=
zf98=DFkr6m1%um+dfOWqAE*`}p@x{3-ARBUgv6Jz+_qw;!%a~^#lgHFEbPC|TkuHX
zLmOX41U+ez2ck7@`LB2A)CC=g5|UGXD*=Wb%F{{*!iBmc$pfh$+=+pKx{Di{>&^@G
zk8Ztl56^#!oB@!$sJOETmmE^UeRTbi04YP`=^6fUwuDCnm!CX^TRUjGvsRtUoLKem
zp@^$gy3OumrS9#~s-6d`_3ZBxCEVCbD-V>$lo1&6$||&5J(p%4Xs{Y1VE&|v6Mpe^v-HH260ohzOGXM+cZOIoV9`DUQkX*J6QZcY@CZEX{yF%Li`k#Doci_Rsxs
zqE6+u9Kalh+aZK%Si^^lqv$RKt9qH+W~+LB=)+Sc5Yk_Zj2VL4KzrMoSIq;(^^pha
z?fPel#Z}%(u>Ljk;R`L7hZ$Hj|5=Q1$+{RI`M;Wwr;SXANViK({nigWho_g!Kz#pM
zVDMAI)Dkn0KW@OkOAcSQPb+~7DK-|MGPO;28PT@!KrO>qK==Pyhn^z%Y=iK;CH<2x
z50onw(E3+Y@ZQ~l@M#)QdN)n23Ynqti!8v9U^m}+sA&;`Y8|owE7jtU#HCsD)@o<7
z{_PR2G1bKaLizt$fa@Ww|672-Le+rxNNjy{7Z%`*XF(ot_NMiJbEL+f4G3uRzY^J7
zUD~$0O(wk&82CVTz*#P~0_fiZ0#8Wl6VM9TYrC^})h2XzUjn}^NNxZ7wg5hn@j6(c
zxnaN*hGf0Fs{p*tx$VTz8$kNMOK-R?{M7l7ofFuh*xT=n1Cm=^O7*Pjy!yh`w!l9Mim+r0my5x@l3
zQ1AY4F1P*Sx#9MsyYKcicqBBETp)bN`);O42B>sPrTOUt#xq!(8-@xH|a353NTtK$Cy?2s{x<8VfX!tewL?!C2B77m{
z0uo}VIq(+^(h+kfuj`e+ZLjO)gVxw70H)!48QipVpX%)~x{vSn*wZI*d$h^{F6R`v
zfvt#!pZgyR5Z((Y)}K}$ac-a(LH&2u{@3eQ?BAyYc%SKh1tj43pPOsaTQ@2L0)LM5
zkb(ic+nZXI0eYx&{NLSy8+6FgfcUK@Z8VU%0}lu}C*kjt1l+?Z5c6#l^dySs&!wpn
zAkQ5XMg)#wOM{V1gCKx=0Ehrc0LTC+0QUh<0nh-@0WbhC0k8nD0dN3t0q_9u0SEvH
z0f+#I0Z0Hy0muNz0UiKQ08j!@0Z;?b0MG)^0nh_505AeD0Wbrw0I&kE0k8vb0B{0u
z0dNEG0Pq6v0q_F|00;sI0SE(#0EhyJ0f+-g07wEz0Z0Qp1dsuc1&{-f2T%Y|1W*D{
z22cS|1$YFY2A~e00iX$>1)vR}1E33_2cQpN0Pq;V5Won)7{CO;6u=C?9KZs=62J<;
z8o&nN34kqt9e_Q61Arrd6M!>-3xF$t8-P2&QveTuX8@i6UI5+zJ^;P|egMw_UI6$5
z1ONmA1Ob2nf&oGRLIJ`6!U0|aL;yqryaIR)@CG0XAQ~VBAQm7FARZtAAQ1opkOYtn
zkOGhjkOq(rkO7bhkOhzpkOKe($OXs)$Ok9@CU4{!i*2yg^&3~&N)3UCH+4sZc*3GfZz3gCP5(%=svtJ}wP
z_62>!Zny{tU+54J7*p%QU_?;i0vQlNvkD8$4;Oyt*xWAy+dxDFHkaly3D`G$Y(QqZ
z8nd}FgN%Tnf(d`@K+xQ+2}4FfgSUj@h0s@|PUyqHqD@Zw1qM|HG29M?Q%>_A?5mEI
z0=t^_z`qW!$R=AGTnyLc%E71sss2ZzSMAikIo}uh;_mJAtm!vL7{KB|prK|GLzn~z
zEYYxk&luU`Ra22hI;V&&{mBaJC`&qI5LbbZqZH(sSboCfGsGJ5$bi-+S;N_n$c>hr
z{os1|R}Owja`4Vqr_SVxAA?DSS068ESH0c89+@j_bLzNxf#u$PXjN$RE#sM*
zxiE2p?#d|**;jA7I9Dz;A=IU`0?g6kPn-X+}wa|KL@
zI#8;q#rDsMO^Sa{*@KsI2NzCqQ^4+-bUgLv5B5&
zac<7QSE4Z|KU7b_n5q|bv`<-G?^h=7XD7wQAO`GOVa1)qd~dNgqUEFb93x@ZDS!CR
zY?4B(+}=Uxaoeg5y>C3>JBL_eaFtw6W^=&WB}=bGG80yvfaKQutHr{G?`;w8
zwJnCj5_lUM--&ayPF%4xH^k?fQn1$o?WE$#hu&jgock?X-?x8LeC%72Dvey2+&4GI
zcSI^`DVH+X-SfhY2zX|{{5=va$8GoAP{;_opK8ysGdjyw
z#d_SMEq$H%du#r5?%INhtmwb$d2kriiR2tR#Vsn^Uv7%(u(%&Kd>m9CHJj?eoKnT<
zU@5}W7niED
zgvJ&S@iYZv$IX)+D4Pn#+1DT*=ZJ?@KR+mgJk-1t(Otcqu=>qm&J)$=Nm>oo>6Fju
zbRjNiNHdIPQ3>}a{w2ZEAD+=Op13}br*dMf@({LC_pmnfd$Ydpv4dXME8p0;CR{5w
zh8g@;YNrh|oLs5XoE7CjKE6o*XY|ug7=ji$!dp`DuC^7Ep6JodJrR%(NW;7Osz_d-
zqHj%}Tu7cTHCP(=^~>q7R+Q!>)#ru=KdtI>1OCp3OsfR4M7d6*1!vz!qg?$=sRFA~
z_e^0tV1}HRC9lt@TPS|_3u*TfZMd1TPS7w?$r%@GQxj?=tCF7{mH54xQ6Y+3w{Ll=
z!$wbW#;Rndae)y<73liGsVm2XBE`7+XCR%5_@$A@rFv@X4idztXY;FU5%=nLd*
z^q`V;oD1d&ykl~&MiljTo=-mh&g}W^aPOCVbu=IDN9ZuO=HrTY`Y9YluKBLmYXP|8
z#kSke{0i{rAMRV&4*Wh`gFh)I1m3QoBEa{xF*6wAJuLX1CF3HpgNcZM-~+8IAOsOM
zpIE?v`)Ih1kPR#V@pi|fWT0EVx`%*}fSj7Z{fhv2&q36@V+SL-YsKDx*|8!aAmF2>
z%GkhIpv}cHAj)PPCm27{t({kHFvt71d+3EX4Da5p4Al=t0J@b?J%`C5!)2*Yf?)4&
z8@a(SkGrk~VKC#{#<#CwlW2JGw=lot@Jo;YZv?=rBI>(MyQ~a&@zHz_0>ehS-Pg~Y
zLkb!1BOrufBOox{sx%9w!sPDX8V|^UIo*BQErNC28gDMEfJNQAjYhZ{=74^ibB7g@
zo-qgkq2pdAuO4tt0AAO`gKo{MpyEOX0rM1i2nZbTw>uQgfo-tO`?uXVM?A@gfNm>@
zkdP(<$W+vQsA;7Ph^RTc7dCy@{~(MxB^#(FGmr({UH{B}SnXY5G=^Z-7`K+Jwe4}r
z082Udpwnx(z>V{X3UD(AwE$iT5jCIBz#8uAS2hQW$GFW*VjaeIS7*nsF#o&OuRAcK
zdsy(g>1H^b69ZlYQcrhal+8~2Fy^}k=VTw|5jF&bjOLs}*b_4FdTv)i(YSQaO$ur6v-+tbHV_
z$gBt0N)0$_jDckMR3G?UG$G?|n8(JPSy%_bT@AyrO_9s_VZ;2TQ)yxECq$kn0@DSjRs(x7+qSpy&QTnjNM&jh0c#!KRA#P
zc)OAWv2vM#R21I
z9^r1I*E>8zu^A{y{rv@W^8)vV_umJ}{75cGDc*hh8w&|!As6%go~@0`pd3O9`%NE9xSd9)S6?uNHUx&IdcUWN*m+>@YRWTV9xc
z@v|eUv#9H8+V|p$^@VYt8GZHqrSufF_fM&wpY)<;(Z>Q{?4ky~AiV5(83#&uomRPrKtzP2HkB)>_dHlNDo%as
zeTP8j;)2|ew1#br?i*%ihrLaKDo3YKJ2gRkfd?;J2hLwO1mVnIOP@R{-l9)6RhT3$nDJ1@#(=+{a-2ixMi>BkrF>>h4TCRZSfi
zID~75#aBEsr}qbbt4}GJ!YbwCbvf|ro+|yO+aDMQF-Z`^V(0-xfATK(bJ{3{qb`kL(ZF
zo+GBf^$u(TE8OqjctbU9=%HN6!}F^Hf7+DJfVtn;OsYV>QZr?e1JAx-g?+;8Yh`A$
zVx!&z`si?n)kg|9tKq-SKTs0N1c-ld4DDmw2+*n+ou)l?TucjpMNPQeHt@~dBLUG%
zg?x%6vcH5mglnJp-HUyZU
z@(r`ti+){=@hS&H_1#JQ4U*N3kBA!|CAS{sB<_XcA>Z$>!QT)jUt^E#r)H=#eEnEv
z-omgl+U*o<9QWP1*{?#=m{R8`7wyHI^i01<*qASG)At3rjV^h)0cNz*kH*FF^{D2+
zjgEhj@d7)>2g>*_GNS9b62)!n!4&nWDb%<%hj|fr$Y`RCZBOMtLl&!hsYJ6T?O#*8
zMr=Diac|kYes(}b&X>ikyQQ(I6f=iVeRwJ>-V!FjIcDQMu)IUOm+wA{j5~Aa^~z_;W+yO*t57)CeJyP8
zXE+yLBDj+VWtL<>xlK(i*DU`{Cxg?@GY@hz+KgRQ59~lAm*N^!OG1mniUC2dn3n8n
zqN`c&4VgA`2}KJc%_*!6qb1HDP2o4qyG$NwZYhWkaTFT<+kS2O6UED0!(Q(y3*6rq
zc|{%+if_BYif_nQ8S{p>I~p~QO58^ycieu6tb*yGh5p+O7nDCt+AM+;*YqKdT^Vw#
zPE?Ym#M4)Ee5$41Udpcdk#lx?`d?@1_jF$9nW@B;tzNXi#72Claq@bhI%_4=6<>Et
z_Tnx)FHs~5J{8-~`(I?i(#-Xs(>0bNP~nJajHe&W!=10tDSFm=1#hyoRk{|KatYpg
zQGl@?N6yLdpll0hU6x1IMMag$HxnW3Yh9wMDdhFoZE9bV5?K!>?|F=wJ+!YVna8mt
zdf8&(rYntZc6vnH>xe~Y|gFD4_s4e=BxFA+crBTT;=^s
zwAPDUi4G6dI8$U6Wf+8<$BQEgcDoB*KhArY&+@$SLedc+MR4($>8{xW6XiG
zdht-8b>sVA2xVzd^rKm{ibNiL4vhS5kPtuZq&t3^^g=N$ZX;Kd$|~iB9d=$H2{xYB
z(Rz>MvxZ-ATDmNIR`+9SFOLxqGsg&X;toV}eu3GSok1I|_pk
z$)3E9O7K1?5$H>5iZAICK`PdL0)kW(y9m#Z()jk5Y<-@wVhcmClf})p8!?Nd)p_~R
zrpNmISFSkv)cOwzqaj<{L$3
zqy3m4WYwG@V?1M0Udjn?_r{5_KF!)dWu)My+DRXwSKV6Bc?i*h4Q`%QGodIROWhAL
zdWmS!b8`LOp?@nZYdG78R(uUK-Io{P`;dn`*TJ_C=?U7Y$&xG^F%uRE;(IVN$_$bR
zraDR3&J*77Jq2FDS5}RRm?aGZb#tJB2LuJBkG?xZk}8Q0oN9G$=D9HMVnJVyOQv&N
znz%s2>#2)^O3Y=-5M8^JGJA`wbiu(dY&0*$I>sly5$S$D(R65IeIT9B_JtEGRIVLU
z&3k2SC?wt8>uW4$+h-v@4=yV3PV!YHd%_iE(iL|_+7f-&GCdB%6C7%~f_x>Z=w}Kw
z0RgLvVQ2Tze)J=z)f3LfX>wB0auG|XqYn_O>8li3i;?m~4brovv)Ik(@hWkn8^)9jiv!I4|suU>5NKMnReeBAaeKH!sUhiY@@
z`rHa$*F(dc>jY(VKS^-T0Jxmb_%>*JH*$gN}K!-AfejPH+297ycC
zh-+l!y@ore27Bp0g!W&)+MnnSswuFt4*Rrb^>F)Ta9ax2hjZ>d!N(lx;6$uNNer>i
zd`sICQpRCDG$H9YCxHeQ3``S>E0Vsl8DIq)?Uu7=fJ(9>#OY1NJKkA{BbGR
z(zD*qA`|Ne@}=gH&aoKh(+IE#&+msf%&BM0^+QL!9LE*l?HNyfp{?Zg!Tlur1@qyE
zc5;#5RZ{OG4idn?7pnN*+P}wpSl7lPRDMAxf7F&e4nkkpKdCQ6Q}B0KnhOnQ+tJPu9h{R*D#uVvCV!fPT?WE$`YbkK=73@j33}kVdV2z3OC?vL(1h!2RLmcUl==X|Eo|_Xl
zu3Ja48s{P)o*(V)S01n_CZg4_R+u-MNnBCHR2j7jR>T$qK33t62tLfJ{;#CSinLGo
zc4x#miz_}v;(jniFChe1;rbWpJ@JdCdt8L`Qk4(%_N@|@YlfI;o}$QD@W)?IjcUjn
zj&jNG_Z7DCcF+ySgrbkf^p-tk$CypiMn_od?CUD$TJ-%+V)+>fEM!4oS(B+lksggj
zdrI#zL?D8vNoyRCG5G4MRTo%2c|#$C(>$MX!`7J9eA?6q^Nh{0@rx>GyjW+P=u=Bm
z!~$uqZ$Q--9Oj)2tWi?bYRn?BacTvdQseU><OR;yppy!vH*d9`2SQ=#mw
z&+6AMKiH>~)(_$ff?Ji;T{D&niaF&RGbZ>-jn*eQuKPo+kmcsHVhP4s`^F;YROAD&
zDNOiN(m8&G>vyRl_Cpw
zL=;EgmL6nL91a%e>1PQMWJ9oziq2z6$30io-%bQK?|)JU9)F3GE6freT(mQh=GwXh
z@9MA*csFj50vOdSpZsdD8swVi|Ycw^Y6zqxUzcc^}G?bX+76^SPSQ`o2~E=lKn>
zU`40jp&nGu6n&4TI3l;jqNztt`PK9*%&A;O
z`HQk*J?lt7{O;G=MoTKH>r^NzbFFGfB09(JRT=j!uC|8bnX(7c9LM_3QUuk_3{H0_
zt&8rs!E2|f2(k2p^B6Xv{Adl1)E%Yt?P}elb^0+U{DO!<~<3f>A
zX^$uADXoxd+92r-5k(Y5M6HS&BBfinbM%0;#T(&Wl|rKde6^VYD}EK$yw+GA3<~E{*9`vSv=MX!a9LYH**X24;qB4
z@k`BBm$=d~#l^r|}{P2QDJwyEG6wtgK!9pjp2$~a-&mYSMvOZ{`>ehwj
z{d_hRWGl6_&*R|aG==)?*DDSl7~b>fH-wAV)!$l54KGT$eH-4qYg012W-!vQq9#fy
zt?|*-e!Z0KvX=e9{LO5-s^wFc0
zkfK3#pk4}gztFmmeJOlZqOljy|57WO<|#h*9GVyYxPpn`JpGYrv<`i?Ec=e0G4kt#Tw&@tdIR&%}QH5bLL>L`(STQTsO
zC6fs{H3RnoS3?1Nd%4&?lXdsvvT?EU^lUQ52+-S3{tX@8Wa1}5MXLb~1cS~)@nl|N
z9wX55ffzt~wSN4qDiLaS8`)$u#%oo2CQVgMp*2g!MO9UtPLW6-lq%-mF8egEn)$3&
zT{|q8z4NACRq}08fc-woP9I}{@Ep~Q$BnIA2Ywa*8t@f-Gr4))d$lJ0gD%+n?9Br%
zLPwSl+Mzl!-aJdk`~;om1r7Xr0pAoi?G_ivVuk|t3p`|YN3<9SEjZJR9xS?3@C4ND
z_jOX*ap1)^XL^8Fxd!UhPT5H~O0lw{=pQHbA)56uP(pYWYM*W}zV=&ebkSeoofWfu
zJ2?I;ZT;pUSd-y%l*u$=am^a(dYfTQjSau_ax|zbqAXODK&wSNgstdOKt{W2Ndehz
zC#~$$qfo2a+LaUQw-KUZ6Ma8IcSVie4m87K{O0ieUW|Q5p?C8l=hfxuddszNa8q
zB4hA)5V`aNkLFN-DNC9-Z>yz_>O+44E94)YJd7JCo8T$h*|Wj2W9!7_B#TA#h
z^MbYLDKLNiN87=lwuW?R^PRl4b*%+$oiAo%U0vtBeo8Gb^Wvmfr$;>UHjNlXPPQRK
z^*WCyQv>&pvlA2xvXrXR3cAR@P_}zXC;6)_VExh(IYZ#PcV&OM%O^5?8IOJm!e-uQ
z>SP~IIwmQi%U)W+{4xv#dq4i~5&hK0uZwX|imHFra*F8I@}-K$$@L05y{3
zXMRAyRW#-7R0`sC8rfv1fx3+H1xS`7Lky$<@k
zL(AwHOrLy9i{?KJ#?we!ZX=2|Zg9=WKdC@Hn!}SP=@5+1t=w+1AsQ<2ZFOOw`k7J3Hr~3vBYhZ!k@SUw
zUh1fKm0YTNlc2>uv>fs-?Q|SL^^rMWP~a$g!x6b{^|e<3K9nw8_Y3*(X9E6u{Z5M^
z`x7vNWX&_r-ukqYd(*j}w-$TVKH;HPyf~qWbr31vq?O<>^g6N~?@(-y(Gq%LNIt?6
z)R3<;>=2NL#&noA$yC^{DC5JGY)ZHK;z)(xi$)DM;A`d)g7CxPB9&w-yPxBJ$A^`aMwjI7<9b
z!2uRc6sc*(Q*bka~R1HuHs3vnHM7ILqUToqpGKE7{S%K9m|06!KU>5sg3@K
z{kI2!Oo6>8OO$V}&QyNzQ0Y4VRvUtDa<_o%g6OFZ1Z_5x>YW8{iq;b;9^-@)Nanqo
zlz3Rv&lmIbos&Dq{J5JQfzvzpo$t;~W<)OU^jdYjPfNr;^{=sY$!E#DBeNQoi0OKa
zbs&xUmT=04gs{Gft;_$^lk)egW<%c6As>?;ghKj}(ri*XnWMA?b-&5noxi_~1@~|x
zgU=^gLuD2z>(!R=eLhb#_vT!FO+6lCEx4(O1?$#cgj=pV;Dl;ag;6CLxkvkB5GLHlyRGi1}6(wXAMW>%8yd_+1@)eZeS&
zm(=p@(@jY|M?Z_Fj@TuB+z)oS@<;6%>F~h%5lVY%k9LAv(?2R+nv6BOyj18PsPSrZ
zi}6y@x8q>3Ww-7!c~c}0(J>RyZ{%Wonr(Te{8DFcx>Kh*%0!|uai%7jGXb~RKAgD=
zKb>RI+(DF*y2EHbas5*HrP_RoMliE$s2_GxU##X7*oIWOy#
zWnR>+zdt6JL6!qIPAYYk$ZlGjrMzbh_li5(OxT_%>H
zH6J9dW|$U@&iBr?x=TEudZK>xOsD~DB57BCNjRdbq&!f&N?J%8BO7cXvUBcbAfY;J
zZdRR)bBao8j}$u_ddLRZKeI)$mPE}!zr0@v4@}d^8iH&
znkq~ti6{5j95-v{=$Wl$=yhhY&DA@wn1!N=
z`s;!5X)g3kd{l+8s@K0XX7{u{J6g~o#hh^Up_g6FS$eb!%L@~mo~0>Rvqhsv|3P3Hv=9L`rUytZZrC
zCZdwZ3d|3P%X#^o13w^R+1qa!gP%R6aUEx*UVJU_O1ssP8ZUk$53d4I+cxsdfq}?D
z!k7@>-bl%J5RtOGz2GKiD1*_1z8KS(x6Mu~&n+x+yPC4Rt^B$&|CgaAj96;EjQjfc
zh3E?Tr6_4r8=udlB#iaVuM4Y%E6^2tS~m9CrQJ=gqhe`qoATqmZ?Zxs3}7jz8uoL2
z-t9!mAC1=?$PTuPJI4onPJExLjPkLEZNyv5e0rlUZF93KSmo&SAnBu$6<;CG>T!}xi$O#;jor_P#OGQa%T`?*%^$st*DfLUkWut(0}w$a-X`fDr&
zZ29Z4xwqMyK}=25!$N9rrtoSqu)hThf~daZBmOY{sg-HX`kaTitS!FzH7^cT%OdeR9+TC%^D@()+7P8-
zDM2=?B$>H2;~y)N3G;<|7e$v?%rrx;8K}~$-*$c!X0dBajXy1Y^02yj`ADaZL9yCH
ziUhJ5Jl>Q=Z`0yb&^kd6-oEi@VcJ3vO&*m|^Ym*qes6IJ>+&N$THxI!WUH168Tq>Y
z#UFn*l860utIC{98MR^u&6L*jQywmlyebdUQDmjM_PS@6JRjP$T4`Uan`qKLL@~*H
z(=iYgQR^!F(J$%&yDzp)GltwEB)sl=@#C>TX9W2(ODd5FQIj7+oZz2d9NEmbI5n()
z=ckgJ_uWKT$@;k1NkZ??*P|eL40*8K=Vc`rvCY()Uh(M(al3*W
z@|IBNZkDIPCk@x*b!FjVgZ4NK0RacYa;t&z;D}ERY2h1Vxc6zmH&dTp7TQP6G)(BJ
zd`KG2At$q_vFeu73S_`yGgaoCi~AC+OsiLq`$AN>EsLHySv(tAq08D-osN8Aj@je9
zp+)j_oFtO4#Ze&Tz|g!UKGaQlqd`TYw}e4z!B{+;nY}Bke>O5sVvp_X3V%(j>DzYu
z$OJ+bB!7YdReD;;X(UBd6d*9D952@WPemNYb9U46V>Q-*(5R%eChVl2AK$w^4PH(QTi(EQ
zi=td9CucUJcbY5-3)$Z>4NI?u?A_-jF)OvXXAwem&$p}NrEQXl($j$0hb#u2
z9~fFovvymz955r-(fRW$#0#=OhWL$^>>&vH$vL#V&@e%r8zp-e?xsw2DQL5Q4EKA2
z8>zTs3;KDIZBZ#n#-Z=~5`izfm{qign`%Z_pXW82nkS`hFQ5Fv{^t{!p4{uS+y67!
zY+82BjC}hW(fPjgX}Z+t>T5&da9mUo77-dtA<~EF8KoR5!Bc1^_XJX5)z@lZC7d8o
zH-6O!1{SiI1PZz~LupIziXBS6T|0Aa+devb5;36}>1(AK5)AZmXCXiBXnaa@#3G&u
zb@#P&RTJKxMJ=ZE#*N)r{P_!AAn
z32*a5;>q^;=_Ngc@(m5H!y_m~h!pZSReu(;M-689H+oL8Q
z(AdIWKhdNiI?Mqp(4_5rE+4ecq7`9&Get#FZk>2H*#*K&uWzR
zWNFc?p=WuVC&Y^vg4<3CU(xaTk%nIApn^G1<0_P~Vi_?lU
zi`hH+i;;caSXVw2{@|i@o$0u4s>i
z%VKFR4`I_-A)jJ*H-(@aj*?E`>!j@giL4Tv&JbbCkX)y<5M%4~(sT{2n;9zhRhQy%
z;F=t*SbqU2QXTW1T*yI|BA9z(2w99AQ(!~3CuzDw*E&_kpALd~>R&v%q3St=%}%xx
zvApy`cEvrLGu66n()L&DD(Og1@CajJ8t!yIPoM!KCU@zlgl^+K!;Yh}|Ln}lo_
zebJe=j-F>HkI2s+rsTI^ABu@#RzCi0{QQRS#j};A1g=5F5Ms0n^qJ^^iUGz1s-kpg>6gJc|{z~m_*)-fYg~`Vnl5sJ4^|b^jNvqOI{(VMxfy|EcsL*g1?-T-E@3R%?BL
z8|}`=FWKQJegDS5N8V~a8Kt^+o^Vo?N#AcF7W+}_O4KQF#5r0%oujKT^4+x4LO1a@3EXpd^7e7YNZ!BQ0)Jgi0Mh@3U8HlsKAlmNf(i%*YU=J5f?
zyN`hhlm-HV?}QsgQ&5%K=nd)}c(cIf?e~x8Po;>|j;T`;G6~4YRgtYctEIBtHnz>a
z2gl-&w>+TEC(=-tus1OpVIH>PD#@@*?mH4Zu?h&EsLgluTn6ub`Cwbcrv1p1SBh=N
z*=+t(eRbbjTe*$5)RU@jL*r@s$8r5E@*UF2LXh2YM*~uq0odNpUHPSGH|$Jt-OFDN
zbd9zRO$FStixsEthWMsyGe6s8kMGVjm7GXwIe1Y^Uwov$VI{@6Z9BzKIM*(y-RV@mksRcwvtf9)NCa+
zH(XuXLp@33(g(y?rH89-?rn?eEv=}0|$34uc&R`hO
z@0yrNaRDn1ac51mb<;6k8+y~tHt36Vo|#B{G$Z2lrQ{-1m3msJbp4(m5vtaXLW_
z?=nFSFYWykdLZcQUB01PHl9C2La56njAWjtns`KUxxHL-I8FUkL{gb41%D0C;OW=+;~
zdiG|pSYBZ7gJ_W-9{29|sF1D=cdwNINRoYFt7{6(9vfV+dBLV|K5wYv0E-MsJ=J96
zP>*{8^4=&PWSYu;y-cBJ`I4S?Dg0hXD0u%nHDb+%VDSOL(*m{C1P9}uS>*WwU5UE>
zHw(Nuj`E}ls|kL7Mhs>BIXo_@6^CPviVr$;XsFE4u~>BmDwhJL&1JU
z^6hO&5yAANWHn@iQtXn4O_}I64y~|+=M2AP{e5g+M<<}04U+4yZX(T>
zw8fiXV?A;&D=t;h%iDjv?O?6K=Xivvs1q((mcRTy+{KnZpanl8OGXnHx?aBkv1u4s
zruiPb!TGG1)_(YjF;C7b6IVO2xlxxFe9*=I?xY4V*6FAo9g+9ZQD&{!*!QC!suR3V
z+QxDNn+?SeRsFsT=qv3#<3cQC&Os}LVD3E}JJ@v7?Ffk_3P_B9Y?q&_=TxMT`a;gG
ztd#SCVwg6CN>e{k>ZR-cD=p9NP13tiomW
zA%SEk@Xd35{+9RE4o~uEzm6hn>{-|k_PsXL&}n!q*74Sj9II99b0#xU>&)56K)rJ6
zKm><&yfN=HWCd+|)Gz7yj)`_=#+XJ-qsWlMbhf_xX(reBDiG5CCH@>k>bTMZrSrrI
zpPr9B+Fz&9t6Cj{c!Y_?nsj^4&5dYX$4Kf`KZ9YaN2!#~m`3az8mcp|KZtx4XHL(c
zFn1H)^?l-wdSG_?bXc;vAQ;DJb35nL6Xg<@4XJmP7e1qO0p@zynmgFd(;|D8<0ICs
zN#gl9E>vUa0(S3pPR{J)gfMXQY`q!<4fu#vid?!UGEs$DM}5ff6Kf%;3-bJu33+1f
zgUaC2-G(8=zG7|ADt*N>-<+lxBJC06)Sfi+NzDi9S*Xt1kala8ntBk9=@pce=9y*r
zX`|Mb*XWJ<$fLK)dZ}r&9<*wjz~~<&B{m-XT)ICJz8r4bC3|(_K(BZ%<&BN)r6Tju(hC*_))TSm_?CY$TpnH8?yT6w-deFAPW
zZWZl46aTuG_h}N*x??Tf8H|%dbDm8w59Zaj+A@m>c*xl3F;PT(J`yVf*3?FAwzs6gil;|L3Z*q;b7H6CA&Vth!
zs=R)jSa0@9+#j674<9djOQYs&EG
zC8bhSLoWuG1ZC3$`jZn8<_E2xes1
z7^{KykcGRej?iT|{VK=4go#U3ESX2W6=&ucRh)!M%IV&h-l{KA_(yROQJTU<;A!1?
z?@u!>^QMdYS+DG~W+q6-Ovm+oT-AgNC)G$}>BO(=mH7g{qnxL!l)d=OYRJE)Y8E7T
z6RC4md|X*dwvV1Udv5p?)=nEwCGlySwmMq>A^SAWS#<0~sM7Gh=CQ!9HuZxKnLZ5r
zc?YY#iL=GRu!ygyAHKY+ky2-wkp<_`c0P}Pc*@xsznyeW-?s4M(8Ean`KRxAWR7nW
zV<+kb5|m43R2pi=Rj-(O%g~=Mr_ntJ?uLJq&~lGp5S$9UlA}phE{L4SU_6qGmy5C_XKV
ze6PlR%1RWrl(DH)_Qk=U!L@8`X2glc@v()0>cSx3hNh!`n~(;N?M2xhH|@c2+M8z%
zd&P*5IjC~2pvq5Hki68uTPCO?_%+NZ;*hURjrNVm8E#jv3(?8p|Hsu^N5%C+ZKJrm
zySqzqE5+Rk#bJQrP~2f~Dek2}aV>5G#oZlR+})u#Gw|^%@4fGL?^^tkmHlMrNlr-4
z$!w`!#T0__cXp5%;TI2fPQ`t2Z*qs~fX*@sjBsE%5`sR`+(q-+km7Dh*;LMot>3}+
zRUY0*m)qs_``OudUvZtBQ`-67NrdJga)>RZ?
z>M^ktKLzy*wZ{3pw#16OyCusv%#^eEo!7lyD&!pria&)3Kmtd6a`Tb)o9YZRpVJ;o
zKO`Ir5n2^hSS(bXP4
zZxGkuT2JF5JKBa1dVIJ2#l5JU`sQD<9*)Km(#Klg^V%;EiG?ZlR8Z_7Iii!5#I5L8
zU(u(7JgpkE)5tvB%<29+={{%t^*@{B&57qOZKQWs5N^>(waZgU350}hS00UGT82g*
zxe0UNi&mX{FxZ%t^fPW}PD+ja?j3)Ne%0>XwoAxLpF4b$!D^_D-w!F<*feb|^3pVw8PD}ya#VlR4>b}XYD84$Q^eB-mK=0X0yv(#Lh{4^N)P%G$HVhr
zqcSg&>4pAf)_C9J_ht79;hkpi-(5>Xs&knTU>hajFs!actGfWXKg#c+00cv5`d|78
zJ5)xUWR&SrS3<&%k}1RcyQpp+=&3Hc6a^=fo8xC+OPrEhw-l~LSce9U@`8Jii5_UY
z#bV3kxMc=;$R){-Cw6H)VzN-&Q!sDqFjq7x?(wZBLV3Jg7z7I*SrbmIB|YYdS$3v0
zf#ex=1sCX=*(H$l4CiH&Buqu>vm{HwJ8hcMfcJJW=?Yug@UgESca2g{@plSrtt)h_66yES4F;tx$*
zHibSpB5XpP0g*4b1G`m=-0a(yS6?960_R0V9>M4`<>o!65!1O(U;Tce4CePbE)<7H
zI2=g>GARg}4zU5Pgyu^p-zq@nN%!4GVp1oiu1@Cwrr5P`-}^SooIWYP_`&@P_m0jG
z$}L7e-%C4i*m?RTOWY+oGqyQb&983=ftFRa)V7@FLu`W&x3we1
zhj&|^SN7!kWAGA&eP)m0xv;&u2pqfATfJG-9Jy%r8^6>e&fOU^Yjz*vhrKTfk8KeL
zlZucFa)4vDf56^nUn%^Oi`u5R4#XLREf;-~z0AaIQLM_rJ1J0giE1m3&$W
zHeT-dYcuaUwYB7eb85GoW>2nYIU;VWLTXN)}9&f9~Wt`f%OS)#IhhPQhpP0D+B#@ykniVvkkKxY&J;>ik3hEt$3
zWSQ86>3Bu%lp2HUq02PB9eC?fFH2@r5{#KI(xCc12EHqeMx5M*xBwIPui*Q|+snO5
zf3(C{x8?RR7-;j%wg2os{>!34kysRAAyaZ?1;5fdFgh|6fP6c8BsHUYd;IiSs?&a@
zJ-4C!8n&*b{Np%y%fGIp+|9)+NaHhJ0G`MCr?U^vsh5%;b-ZtfdVV>(D0CV&$yt(P
z{Y}u#mi{{|{w39_^ZFV%iXR^}&*|?Q{?S=pGuz(s@pB2yE!zI==)58i!FY;Qr{OH;
zABUY35M|QpYcLIvW9P~o=eb3J$(k6^8v5ssI_E2Kksi_!|6PjoQu4iys}GsYPa5wQ
z9S-<;?+We%Pn<6FGxo0zo~DF%tz&_pQYrC{HU}J?(Rj(3U
zaQdZAaVTgM<|GL*Y=ND?G37L!vG<9QxIa5(55oa^t&+X}=``U1iKRKr@X_0LcI0!-q989~
zjKEKOT?byv{51xhsq2#RH{K8JN{vcE;_Drc0S{F1@slgj-1cj(d+k$VzMFvOkjz1{
z8RE=_bv=trI1eRa<&)o(JMnn38TnXCef?k8b9Ug<*JT_%m^A3EPoW=%ICs{@v6X&8!R9S>cnDIuHLP{V$oc&MOZ+@_Y)-vTCuYRGEIO&&@U@DN~(9;#-KOb8mwutAA*EWbb
z>d17^c>e|5OrM&4!p*5}l!QhF)lEGez1i30%s%bB8R>=pdEz1bXMNXt_Ehuc;OY4I
z1cUU?q^^qWVJqTQVa2OME;5)erv@YmjR^V$15SL?qY4k^d-I?TM+Tq2T|6JtZe8br
z0x-cM48WS&A=g&dZ=89A1o(O4$gO#Ms~)P$lq7j=Ga-g8$7f7#oshq#am?1jx)NA>
zbphz+Y4K4PFvGMBfE@26(%V~Ai-xM&FQA3z+8R~o}1b0R3K4ns9E}VpZboOJd?PSA41oxi3S|y
zH&3LnQap!??+m4nu`i3pe^&mP)-Coibyc>c??1|f?M`HE&rs4Jv+&_LPNWfKZ8Il(
zq{If~t0fq)*f5miWjue*xNdL@V7T?*GvjitGXzgYP1|8BWU%ga;@#JkFnAUn{P~Hi
zU<6!SQte_QdyGZHAoU3M$@r|C{lzA^#OCJPUhywpF?XfR<3aL(ojy^1%a$+1FnbK?
z0rdgVd0*b$7`XR=^QQL_`X4`e{Z+(c*n0Fz3xixM+|<@unJaWxS+JMfd@wTAjau)_
z7n=S&6~|J3tQV1D_D9WRA?TsZK-J$}}|-c{}W0m?_{0nOe|>bk06}X-=4I%k&(>+Bz&Z5OFR=
zytmWU`8(8PaOi#)^rRF*Tc8q{W~WQZ39c;$$_HL?Q(F+jIK)Bn`fo>_`|kpC3*R0)mTsrokcRt;_i>(O5v)y;_+CJx}$hau?C
zSkWJpLmuo(usOup1ti?;RJvTpzdw(^KPLf$FkV&zUmSaYqy-_Am<-PlB%dxu!C1B0
zzmr&l;&N?|FyTGvTreHM)4&TgB3KjiMPN7Gy)61n!!Hn`P)b1b7|C}*iinf0`F6my
z&NtHFWryap1$E9
z0I`iMxU(|i*;zqk)4~4ioZ8D%LzT{G1k0}(sUWBH;1M=+`Q6{e0^kLIu^+z5!>`@0
zB1Z>-nBuC=&r>iN55n{o(l}#&y_K!8Sm1--o-OpL{g=;Xsq;+Kv`MC#pFygh8;x38
z0Tzy|CnYhG4>$#fo23f)t0bw(t@^oE#DImM6`cV%i->Q(zvdH;aIGcha#Kq9@WN^2
z_$iq7pM4|Uz8F#EHw8MsBQOhL;z^O+W>Sx`JA6WvgW8iD>oC
z<~aJ=4j4NN(icu96wm)N%lz~X$fm@u4BFfwtL`x(rtSaVJAVi8`~Z1
z9?k9Ix5Z?ma!BdDDy2uqEiMl2pMa*wF^*2m(Vv=2oRQs6++WBG$-7kDVIPUO76O?O
zPTp;B!$QO30)as`p9K$L6JTVYVBj#qfn*Y}n1pugstw9fEVHl;_dL@uylD4)HWGs9
zX?2(c)OkiY)D0+bkwIEUu|$c}BXz`~3hbG9MB0+@fDU*8*+s#j;~9=oemS3VAVEO@A)n$p9Dw
zajQapELAUkiPX=eA}>3H3dn{sHiAr%OgKd6OGMQ_M&wI`;wouh4=%c8J`3jMtGKu8
zR!*ccn9hD*{oeKa;|~r8{ni-k(^95qIV1-OJR*C=g6@$!Nm*Zg#rI!Mt(m!N=|f_e
zO?QDL60lk+;cy<_-@3eL)8?IZ{GE3ZnP8%^G?8frcpAPW{!q2~>`oxY%wwC|(_(Y-
zk>1Rsu4_vkPU1WM%=VzoX8b%sOD(TQjVssVlhZJ2FFXfIiDABGQ~jv^Vk%5RtcfFT
z59l)V!T~X~>lsf8Nrx>QtIo4`%05$krHBZqgg%({S*S&qF2cGLmOTQRd04`z
zWe=JT++dBK`mkkhls(2=t(mo@D}Y@Dfy0#ENhqxagZa#(;xAM9B;cMqe7%l%;kFjY
zBr=jQyjLr3-_dg(73tc$fe_XWGk6zs8(T#!Tik6N){S;J+hZzg=K!eb8JCrRKo7_M
zUgOi(XhMY*Z|31M$YhBhnXfDOX`#g#V?S1J9nkTt^Cj4Kw7LslfJCas7uzbTcf0!UvO>MUNFGIfHeDn1QyF*
z7glDnuzVT&*5Y5S(HDeC+Vb5R(7q4n>N1^$8|&`Fk_kTLruvU*oSZreIWcNXBi%zk
ztwh)5Gb#qQa|NQCEfP_VGSm(esab{DQw$K+Y%eMhsrB^V$&?;o5QTZwL7_EaxyFTH
z+avGPZUclxpe@ZF!YWYGYLmNxgXNp{doup^#D+t*iCDhJ?wW5k8kJuJWezx*yeHSUc^$Lk(F9=PO7r|i8U(+>{8454^etV7jExQ}
zg2ZyMw|su_qoOv>!`pa$`AEZ|=l0|#XQsXei)t`SHc!2!BxRf2x^1<#s48ps
z5(SA8(=b(jv`@%DQ0+1FoHpYJ{2n0ZJKBk?uub)5qP%)S{w{}w3F8JO5Ebqks#-Vi
zg=n}~3d0ra*3`cBGR*UnY#7Z^8{il_y()s@M3!iTpNeA@4tH^!D|Hn{`$WgO>%RYl
zeb*>vyo|)dZ5NN~lazhbSJfs^Mn0Od1s7Cc}XF_N>M%|3H}
z1f#FCr$|T0P^0ulxGn0v5AJhSk1l^Bvc|Eo
zJOC)$@Qae67BLr2=FcUON!aEC(OS%AJJIV^HMQ?j@RlcQ+sKCt@nwqMVnzED5*RDy
zOZoB>F@8k|A3~A1b+qQ|?-Y5`#9bS3O!Gl0$J0%~N9sI0sc;I}GEBAsj!7B
z-wWgEiJ=P+7{BD%L}qiEi8Dvz_D|-J&25kAJ=)8ejPckvy0an8h4*uG5p`@cT#5VS
z6LEAvJj?p22^gI*Mdk(#FXW}OE)TOaDcvgo(Kvd001WucMbes?5n3qukI
z!6qF>?Co@*J0C?@3J4$O4hT0lPXM#XO9W~ImuVjYbgeI{5za%z!9dE@FVW7k3Ul5;
z4$}6dfV`*D{vy1X_Z20&Y*)!bwPQ(hHlpJnUP?c;%~ytrAv_4@SWtyuUi-d>R0sM!
za{U${=v`sC^9J@g1sbSr`={bB$SL#a=MvIi*N=S=8kQHR7*WzTItD^6DGxZVtRIu7B@p;RpxqW0gA~@$I$L0gUr)qBvcD28E4dR
zc6!D4q*qk(D60o|$$MpMdb!2n*?;H}QFiYY@HM7;FQWX;Wow*W%#fE5Tj(#1f@zI0
zlA+cdYb@2ywe03sC)z*vf_F9PC#lW@hW!<8xV)9dTiS4^_fylL9~x;f
z6EZuSj*aiwpK!6g&{XaeH^OU?Ky6xS93|z>9t&8Nwv-*{-hgmw(KYx9k^C|bpxYm6
zz5Xd9w$Cz#8~mSTv{TTry)`Xa`%I`fRGX0OT@bG<)D65p*k0^-@qM#mark^d{j>YU
zASM8Mjw(gxJ#KkV-@&BhUmMON=HpG-B7}8#YBbLtb8FE=60m4npIu`
zbNAG0Ebf=6$Ole~>vgJv7XdcYb)Y2rz-0FY588PTSLo(nd(O7db6Oi$z3i?(x`H|!xTVlD@|sW)*|8)s0V~4t{WFYgs@seX`;4IBN(Nv8`!ZC!4b=6i
z7BaQ5Tpkei8qMlnsx7K#58Y|O`UZRJC)@lxhj*@aA2DPeZOO)o7uNNnL4RFK>ya$x
z<5_JFxkJX&URX
z$dfdF9n?D`OQWAo!wS+Bu0_+oYc*>p0eb?fW%=`Kul$%RCewi^r}YL8-qyyyc5(Y;
z6PIyh_nWRziG~ci5}LtgIF}YByWih89XBjE{#u!fn#`wtCL7J_XV@8{O>~OulcYqJ
zE2|M8Lv0+gAfZAk0UUf-zA{9!yW|Wbr0MLyk%HnRS?M%;ykWtc4j~51AB;2hni>Z(
zalw1{XmFw1yyybO(c6fAn?2v$`@`m7u=`nt!rmcto_aWSr}D42Q-`0g;p<(s&gJXF
zdBf9Db_E%*()UupRejqL=SnJ}K36oGfbGLEA1#H!-XpEF;Wc65q!IGi+-YX8m-T~k
zjyD)ixnRxjqxDN4KeS{bWPXo%Re6W`G=CmXWg5a^L;g;YsCPT1wJ1C`czgY>
zwf(b)FKzyhfB@`}6GWl%FO^`HIAB#T3w3~$p+s5Vj2Cm25^=M)5Ks5aH*q=XN3y&-
z2Azl#wsutiwV5z+)|yGh6IbM0y=loDVx625$#+W?;*{@)urkzAFzbB@P<1pEvpG3v
zzYi}A3%te{$({v@bXKNEiy`#%rKQnQC~TpoHLk8CP%)e8Gc|ZDc_uKdsbQ
zo)sJ3{-_2xuHb`^^1gK@N@IvhN%qIDYm{1ZHvIljajsNPLljmNLD~0Y4lhmpBY(}~
zlx>;UA&eiNX_vBK04`!GPW
zu?+d@2m8rqQBme&Hd@`HHA%CN5y!gJ@byvB0IH%?TBvWKS?GNA9||G^3J@D6hnqZF
z>$#6F?eTu70bqftnKXZE?sxbiqy9+pm>`M)A8xo3)+1eiA$#wQbAA01O8em8gx5)K
zc|q{4|2#%ANGOqbT%+FND7qP5&QDC=ffbogiSYs=sKoy#7FR@>($bEJ(TrC*G-rfa
zGiz3iV`i^mGu&Ej#yQugM`PpWHQa){mJR6L*tp*rZuJ72
zaVX1izVn9}>O(4VU5`yI-^m8N#(1?Bi*d-jWo}yFKUK2#2EV!jKBXd_96tjFKgH;X
zAbSg5_y+xzDDeA5DbxkamHkEb-Z99w1OM|ZzhF+p2Fx0Y=OyB%`!kl`m}mb*rJBQn
z>%A@|MK?q8SP;N|C_%ClsvPza$-fW?Z|lHtMb@CH-a76neI4vqvYau{nOF%nfPUZr
zDHW624)&hT)TPF4cpM7dMkI2aipqJNkDV$`n|H*zt#Tx8y@wAxN4FA0Tso`-TZQW*
zgqO1HFt#MyD_!Nhi(9Yw8&kZTaz?kv5_mgZ_TA>Rldd)SbZhjWLE9yBR!0oDBIePH
zeMW(-s!boErY)pz>K34;PuRUGmU=r4CeeDr{7Z&uobw<>O?X|zPGM$upEDF$dpDV
zPJ~NlX;op#Y#2Q)tW!0Z2afd3yJ_lK^%aGrt}1-|9roL~+OK4$>ow8FhDYxA;@een
zYMm_)UHjhCW=ifOvHMtV9SjjY0uh+oy0VJMamh+^+MgElL=s`yBf&(Sq1odcgZF$V
zvL3t+N_8qjnRj)AizbWpBYKYrPk7rV_3bB&F{m6Z*g}2o0#|q&YCy9it|@c
zfs4*AY;g6fxuFy?5Fu`RlEz&WvE*uh6YUu$Yt$rE>0ZeR9Z1Z7^~sbMi0^UrXM_1!
zxUGLZ@b2E=g7ihi9PGewFTw`jIeYKR77ZMV+o-Nne8$unH?g$(8^5>7o={{PU
z2$vPiTsk)FxS3wl<~M!p=(=^Zn8`3xTK}fR^Cu0?9?F*Bvyj@(S&vKjFQ4~ko02RI
zh3_#N)7J9{E`efO;5DMNAH<0j8EF>UE96fSf%BgNY
zf~)KWN@udfxPm6cw95U_4DGG-iiE6*P{io6xV2r_B;1`<<-eu);wl$S-b~RBvaHm@
z&k+`_AvoT!t6`MPnc7+3e4f4ISNp${?
zh_dB<_sI<^i|>cR@(@Lu?442P`)NXzXq{;j@=uypaXo_>G#C|aSv4Q@(xPp(tAf2K
zVN4v^`lNtn>Z3vX1{f<B?!t!o+g!}|aR{X^~vK_v(|4|IJi<JEDL~dzxk>z)2-_ke!$+LjW
zL(MgW`WgD)53A!FeL_uL*g6gTMSJ{kMdaSmDW$$xsvdJ0jWoS;E<2=-=9tYF
zneEc+)XuYXjPQGOa=#oOkbTvMV{I8gYKmW#+DBFSW|R2?mr1sZuM5AEcE!L9uRy^v
zqbjYD;@p*FIxjZSXXdnN*-QE@q^(OSA2Bl&6_yl^#y;NsHuWu|r=_v*m7=kz)(GGP
zdU8g)-S|9e#;sOgY4sO2TsM$;+R8VC_l1z&Xg>N0s)*v?#8*Wadz>K0RGL4)DUB&^
z|3r>!Uv1}RPR&n{~-KD*}Xim=NgqMXTC3o2upY>~T(oH8%S)cXG
z2S<>IN?Qd+8irem0rq7Z5#@8zb`*LJ@K1Upnb-HWx-D(Cc@es`+*^;{`Lh@3+2$=B?6F&c&^RmSrHtRhcO%#t#2`u{Yz@E+ML;uT}74t
zJxwX)v_SY`)OhyRf`YK5qN;1jFJD?ZUnOu!?5Ei@-oLX3P5>TzUMeY@E(d*=lJmpn
z8=`~Zmb2>ww))&pEeqKlMjQ=a&46IRnCMfNsO&)Pxm;D{Mb-`W5}KUG8QVciZ0D3m_Q!>
zJG&zTVikS;USEM$8|dHboA9Fl?lCtAr%3mMmJ{`FmSEOTwd7Bx8XED23@SilnKf_%
z823`!+krDp>Pn-k75%Y=Z<>Ld?#s}hE&XNcFCL*gVp1F%0U()axotm(8R?MfV
zs?ORmN5)HyO_!Q3$Ue9{318?%m~6@SXMaDF_nc|=*t-*#5BTNph2ZQ$SlM5d;77(@
zzUHV{8DE=>j0;!
z7TTpDDa+-PA%A!ScJq966kJwxE}^YiTSPDM5FdE8Y3#E^YP<5^qPe2P<}edTk&Fd~
zZcnP$sGn#C6!VYxw9eKJ5Z-%cf(a#0*V!-h+&-+YL1$K;MJa^u+(`o$vayUKq)t)l
zuu_2e5}h+S>l7)7>H{-yo%GE^2?I7}hwo}HeD;>E%aNmv4XB<>SxJls{?6!*PTn{4EVAL|iL$Du^5>6S
zL*@8)*1{WUhtcdhZKYKpl#R!)uL!RnoGAM;C?!98LYP742nXgR$)VZ`l1oXw@z1TB
zYAhcoswDc<+xdqS|Afcj#y#l5!g)|+jf`KBMhE(sqv|_Ekev?Yf!r7Jf8sC=D8SQz#G!W7kKcuwmCHQC!-+Y2+(*hxP5UG!DubHp464nig
zXEpg!(WUp+2*FgMIg`alq;USQ=$hE0!qL;ER5(|h9)wFS;HjEp0WZaG80H5^54LBg
z5oVWZi(CMm#OK`sgl&&)RUM3Rr)a&1&D9ImFlDR3awlYi>K=m@NouuCR&6hWPW>>_
z9aWUwp$Y
z`|l4;yAuzn*@>swNmL%d{SjhT`q;026HR^ViygK%zt>n!`I+yl+=j`$C*3ucUIMdh
zH&!BSfi#6Ou6dqeRj7;^nf?^+rnIx*96*$bArC(r^QSa)7Ikr#zB67HXK_#1mUTmU
zNjJOh9tSu183FI~8B_3JADfrzSh&fqVsVs^GT|Vw#yCF|9lPj6SJ{AVWC2H`Ta_Sn
zjFEt=Q@3vdYp^!D->iw^ufSLO6CI6y{-~k~?T_{GGU!kGUW;mG!~#2tYg%mk%v*SC
zaupjP(qag6<-X-mYwSl?MJN$TTFAja^v*e%jDvsVgL7sUr+Qo@L5txE
zy&A6PXycZ#cU7~RG4)ycpJ>FLQ?uw5x~7qApi7b82m55TujDTAUg`mK6p1-sv#(q)
z6Tp(96*gNMD=tO+(xO%9_-Zz|{f-XB3kB?0zXXze@}+-QTR%UAX;am;Aq!S5Eu!
zE_I{ye~m&u2iY}=j<^SlF6}1H8txArRw?bG{lRxhJ9ZRWzDdYB_W4Ay{ivAMd|QFwkK!i(
zNYqv^8A(nKNc>T_ZqdkW5qp?q%&X18`!uqi%$EixRW
z6RLyNYi1PN!eR_b9Q@tnj}~H-JUcE#MTDlSYgevBx!@_{Ypw5(iH~^7%dqE1a)(;f
zMlZ-HHw8B~Q_Baa5r?yvtgEFV@BG6RTggL>YFPgWI^Mm8!tT#LmckmaHX3~%DfOJ#V_Avy~Bm|+J9ts(7ezvl#$;kPD
z_ILM&h?zK$-{rkKOq{Wn38NY1ud99)_*H!g2%S90GTNlQvi4Y8&;!V)3fUPF6r1WL
zN$$Y&NxX{#BJPT5GNRw3GmzLrnLllMHDR*&s4ZZ+BkVLMle=<{_aB@?@gG24=hXxJ
z=kQy6(Zu5u?F{DoW_Gp_q!@U!)L$SQ>VFO&Nx-c9M^Jb{t*H35D5;D<LQgwzjrl
zwH-iP7uGpC7SN_{DhBvWQ0Uo6JQ-GJ4SN+1_o)gOgX3Xhhj1XtJM
z`E7p>C)@^Qomykv_P;4qLGNJvnVenpR{ia=fm`nG>+s9NFeGB=)Whs(Csr(6p$_WEpPsi*Umgqz4Kw)phBi5>dfbs(EN88o{
zxzoGBILhpHZC|FC3Rk7+xzVA;^3$F6e9t`voG*laVG&D2sA@HCSMJ81AV&S)B*TJ79TJQa{
zs|r2T=svl3i}~Q#z`0~!meyrqd*f87Yad)$#>gVw@JqHQ7}=0&sT-
zPWftOd_!(eb+4VsVx)L-0uy|Z(k}rCk0dhlK&vCW`M9jP$*sGK*d^3QAtFxL^bY98
z%JswjqfSsKM~t6Qj0uQVpjjEx{+hf)5dukm^o?cl%(`D)IL
z@_%Ilas+@!|0jQp19;~xiH);|Jj$n6^WrwR9F2SeXo??AlCOy=O_AxL;Xo$tV7oUX
z<8e>0*PD^CydSv!t?oGhyz&+_bTte-_*PdE3C?{hDMo|;!2B~{CYI2+@p|o{4=!hu
zodO#7)g+n}WE2N3coWaS^ar@V5dfsTMi>20oc=0C%Ta6pFF~6@A{h9lay1#e_Ezdh
z1G~MoR($`oK=s=C4mSOg@rfdRDiVwZa?Airya8)6!830_y*P0nhgV<@T+Y9w_y1UQ
z`~b(l0oC)sJ#Ron%Xlm9*FdSNXzAGfPgJ1yg<#9KVV0GHOWrU%#^{K;ugbo>PTH%L
z_Z#Q;HDH7s#%@VyEKq(W*z8R&Vhy
znF#HuH%^o<@c4yd~k{Qgb6GN*~~{}<=1z3RYCM@NC6f=rshG;c^5
zcjI--S4Bzx7dKG*FL3x9A=!3tJIsG6>Olxy;0hS@*OK99dLRL*UOa82{-zS7a>S
zeNAVS4f{5hu5mDN&QiZ5G#aR26nqZ-4-|8`n{f3Cs)WtCeC_qsN`44T4w@PVl@gSO9b}1NL|m+suRO-bxe;uY~@MXle=E^~M01%3~GQ5*el4P+gIW8UDbXW&7ie*|w3AbtO%0vVG0Hj7~=
zX*K^nudnGZ2oNq%3>qZp4T6RF3i;Qp7#q_128Q#u*tB{DNBjro1ZCqvLf(krlFmX0
z)4t~X`t<+)A0Xv)D8Hs(2AL8&rVV-30T3N1T
z2=Uf~pVlQNq1p1w0g-?)`;QbU4u4kgny!QCw&a~oX1nval`|ZB6Y?|&ckAqcYyuw)
z;Hn05=PhbDDB;C>FWrhSzNQ@d%tT^*3Zu218MTq;A!Eo3cVY?f(0V?yY6ZB*xWpVT
z#5;%6Rtqc)a3YJOpt&CQNL|)**>BFpm~H5(vZpVO&Lo(bw$L$f_j6Zni;-Cnou`@0
z->WR65UJkO`Fe74M%`UP4+6-6lv_wrkNAHItX9zr9Zo{O>@36ec8sQ(!+?xGR6?{3&76FyDe86BkF=x@n2NPD=#MR37A$S5q{Z&l44>L^p*Q;Kb^RmoBA
zNJX$b<+SzPud5Dx6mP
zLGg$EQcvM?%Z#4i!>^04z^f8<;o`BI?Yo02fMKtDT($ek`=HIO%)hDxA=-yebCXJd
z7JDTW;aXi?V*@K)qK&oNBRK(YtwDiinM;T!2wY5pFW~d;PwV1jir{aeiO$;I^f`DL
zftKatTDRgptjSh<#fRtM-JwiC0b9GfWHGTO{2x3I1+TQFFTC5(!2b8cC&v!JE;5yL
z;mVOQ#b4DyYp(hGCyVNh^W2IP?Ufo=rOcw!xseP$Q0jj*V83Pl%qI?;bk4y_r_87$
z)~(uh{Z@Fd>2McX_|@?s!=+zyTJ?NbMu&Nm$ZnU&p>dg
z#~$@Br&CG$gSGDiZh8P>yo9XiEg=VU@rMcKok^3g;BfB#=cI*r?))Jc8iOH;{&@nO
zZ8bABBu0V<0hQ7&BH2SoMz>&qG|FdXnU0AD$l|TGhNm;)WPoM7d$m9)c8Q9R5qi2v(c_22R1
zoC%3|+Yp{|A+QMlwgKo8h{D^3pi&OGdkb(bR|9E+{urVvKZ?a6I1^N9(a`+ZP|5kVT
z1j&Q_*RI3HHt*)Ohj`q#6Cs^z4T6^*kNFG_8he2dBmGlxfcWh4)}ayRv+`SA1>rOP
zTi4I>dljRvDkQQ0b)Cb$ED4PRLZWz{e`~Wr|J?l6Z6@pU!5fuu&S(BNX(#`)8QecA
zKK*zntgnd&?~wm@u*e`Ck!N|he=PDOo(JEyX*Mx8FYf
zb?e4LdeegSioypi89e8`u?;YJCi$NcSUg9+v0=7(rhOZMn$xq#TiuY`bNm}l-{+bA
zZJxdcKYP4A%*g=azk#pw^aVRT(ChUP@kKrtylGgDf5v(%ouxek-i{Mqr-cKS*O48e
z|Ci*EF`X*?87{|m`@grateod}Z&QESmdJdI`ug%j_K)~KPt)GcFL-3vV9(S*>ny
z(H7ENIs36V;@|)eLt_eyqyMriNR<>9Xxl2mg<@
zN|pLX#_Wx^06Pb}(<2mTJ=G+gFdQ$#jOTAOZ2cC=;*NK+>A1|HzSt6phJ(tN)
z`Zn>H^l(DoBTrSB()fV6H_HKD>I~hiQo-@+_7jM*?3PkBZCK9@j(<<$vu(~x+UJA~_V1zFyh
z;s9S}jbP5I*!Cd!Ta+G-u*@so8KiS8*fS;G@9+!DFzET%ezum@-dxgD=M|ia$
zsuFvEj!<|Wg8XogGN-7!vHee}(@DwpWJ*ecF95{3b*k9|n
zNXEjgCGCwQyGbeft}}#fp-zI#N86yTGRsA7jHLSRD7^jC;?EKs|8l*!U0v!tg#mBD
z9H(dL-zvTGx_KNO;fVOHT8(TB$_C}toK-wpqxvFCtv*PsjMr5O;!^E6go?WK+cfWR
z2?%PXgByVA^+_<_*}X5kyn%5@3|=+*1O29taDo>wkski?yy#+^xwVB*aOR=jM#obu
zc`C9*$HeV4H%R*zW7q~NF(;#+;VtKNr%-ky|F)6VX!I};W7{*#5mpXsjE}K36EIt<
z?&%`}0;sEQNSVd!X{ijPGL`(iX`wT<9(TMMNhz0`Pm3D(`z_6h`H9z~nZ2mry!=p(
z(<9oxAwpfBv2-T-({MAHuh1^ja3PD!WLgJA+n(o(;$e=%)^|AH?6&0h$
zG@TiIxg{T(Y|zT`LX&_NrWM`llMEdmED92jwaXE#f`Z&@RR4#ow+xD_>7qvQFu23u
z?(QDk-5mzEAi*Ip!QI{6-5r8UaEAcFT|*KaE>C!G-S4}9sIJpJySvY!YWG@u?cL56
zE1|qTD=mf9)rE##ixzk?zsM8l4%R9RVq5TYKMtuxh$Cl~btyvqj-L#}4)LJHFgRZRjl
zuvzq*a~uFrSje_T%Qe4p8#s^8K0m!}X|DOXqNMZV8mw!N{?ctUOyCr^y=^-sOZyWJ
zZ#WYMy_SY^|GO{j%63s3s7rEK|C)OkOnL%Gt!x9l?$+lD)!;~^)0YVN(iuf?jdI~A
zQOj;!AvdvYx1odS{?~9cdCdXOdD{dc$YactXLF#EQ=NZ%yu(y&a&_(rhWYSwD?{-q
zLTJ|VweULS_H%!9=7y=4)(D{T^w;XRLRd{|0e-Y~k0dohtgJcUg8<-Qn
zxjyc?NFPhF@7iw(ud{4J0&|DoN^;l@N3yPZ(b(_1FU$Vy43^RxMkl25bhYwy?Vl{-
z1B^8KW}C(0s?Rg0sQH`SMOjzHJ~$;nsJfA)Tzd(MT}h{Nx14Cr=@=H_xl5DZ%SofR
z@vj9K7$;p*eM#T_eiwqRA5H*Gk;a3ar@fGj9KEz{{u}#?X1^JGPsj)&C%EQ*kq@3c
z)lnwBV;HHDIW1X2_aP+8aR*SNA-)6APtPGGO2ob6JREomANWk!!?Z2H-V=GrW`7@q
z=kUXtOVL8-H+mro9?(RR`q4j%WJfyabBqq|EsGR+C~SR_z#Rw{-c)$Xzr-{BJBsN>E{!|)im^w;|#BRsye(;8ft(E
zpg43@UJ0wuO%!ND1AlT;DikWnipUuWqrc9MC=WlJ$QvQX@+z{~VdUZ`=SFHZTR+M%_Y@BD9=#3xnWh%ebUi=caXWULX&a&VelfNy#VjkQ6$bRmD%
z1UUh-2EG<<;`mbCz3fH{#>NPKjAr-=0C9@g&gc)k-r4731wWVt5%n&5kH
zrv8>M{OvLp!r)BBtW=j}%9$Gv0y>TghZz_$`jN9ODNtIaT_)(QKzHt$#{`YK>Ll$|5t9hw4O4Xq6RG$unyoMd8
zI);V00`nggO#?Bo$5NeK95%+_J7VlVub3{n;tls`u!Bi})GAn+q_Lhkmc@Qu;dL0X
z9vnbDPgx?rlcH*jPl+}HhW!&HG#lG!nWdT!jffR8d%{95<1DpzsTWDXZivA_jph=E
ziSE2p3TjN@-cm&LSX$Y3Ct89-d?M6g=E9O?&C9k~c5A16w
z*4!-NWJc`DVCnwQ?Rqp+8fanjo6AQzFE<%3vp5fH2I_H6=B@BcG9-gbDhB4oAz
U<
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