Merge branch 'merge/duckietm-main-20260407'

This commit is contained in:
Lorenzune
2026-04-07 17:14:27 +02:00
18 changed files with 1262 additions and 1495 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
INSERT INTO emulator_settings (`key`, `value`) VALUES ('wired.tick.workers', '6');
+71
View File
@@ -0,0 +1,71 @@
ALTER TABLE `users` DROP KEY IF EXISTS `auth_ticket`;
ALTER TABLE `users`
MODIFY `auth_ticket` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS `idx_users_auth_ticket` ON `users` (`auth_ticket`);
CREATE INDEX IF NOT EXISTS `idx_rel_user_room` ON `room_enter_log` (`user_id`, `room_id`);
CREATE INDEX IF NOT EXISTS `idx_lhcp_user_claimed` ON `logs_hc_payday` (`user_id`, `claimed`);
CREATE UNIQUE INDEX IF NOT EXISTS `uniq_room_votes_user_room` ON `room_votes` (`user_id`, `room_id`);
ALTER TABLE `room_votes` DROP KEY IF EXISTS `user_id`;
CREATE INDEX IF NOT EXISTS `idx_rgs_room_ts` ON `room_game_scores` (`room_id`, `game_start_timestamp`);
CREATE INDEX IF NOT EXISTS `idx_rgs_user` ON `room_game_scores` (`user_id`);
CREATE UNIQUE INDEX IF NOT EXISTS `uniq_crc_user_campaign_reward`
ON `calendar_rewards_claimed` (`user_id`, `campaign_id`, `reward_id`);
ALTER TABLE `calendar_rewards_claimed` DROP KEY IF EXISTS `idx_cal_claimed_user_id`;
ALTER TABLE `emulator_settings`
ENGINE = InnoDB,
CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `gift_wrappers_new`;
CREATE TABLE `gift_wrappers_new` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`sprite_id` int(11) NOT NULL,
`item_id` int(11) NOT NULL,
`type` enum('gift','wrapper') NOT NULL DEFAULT 'wrapper',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `gift_wrappers_new` (`id`, `sprite_id`, `item_id`, `type`)
SELECT `id`, `sprite_id`, `item_id`, `type` FROM `gift_wrappers`;
DROP TABLE `gift_wrappers`;
RENAME TABLE `gift_wrappers_new` TO `gift_wrappers`;
DROP TABLE IF EXISTS `pet_actions_new`;
CREATE TABLE `pet_actions_new` (
`pet_type` int(2) NOT NULL AUTO_INCREMENT,
`pet_name` varchar(32) NOT NULL,
`offspring_type` int(3) NOT NULL DEFAULT -1,
`happy_actions` varchar(100) NOT NULL DEFAULT '',
`tired_actions` varchar(100) NOT NULL DEFAULT '',
`random_actions` varchar(100) NOT NULL DEFAULT '',
`can_swim` enum('1','0') DEFAULT '0',
PRIMARY KEY (`pet_type`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `pet_actions_new`
(`pet_type`, `pet_name`, `offspring_type`, `happy_actions`, `tired_actions`, `random_actions`, `can_swim`)
SELECT `pet_type`, `pet_name`, `offspring_type`, `happy_actions`, `tired_actions`, `random_actions`, `can_swim`
FROM `pet_actions`;
DROP TABLE `pet_actions`;
RENAME TABLE `pet_actions_new` TO `pet_actions`;
DROP TABLE IF EXISTS `pet_commands_data_new`;
CREATE TABLE `pet_commands_data_new` (
`command_id` int(3) NOT NULL,
`text` varchar(25) NOT NULL,
`required_level` int(2) NOT NULL,
`reward_xp` int(3) NOT NULL DEFAULT 5,
`cost_happiness` int(11) NOT NULL DEFAULT 0,
`cost_energy` int(3) NOT NULL DEFAULT 0,
PRIMARY KEY (`command_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `pet_commands_data_new`
(`command_id`, `text`, `required_level`, `reward_xp`, `cost_happiness`, `cost_energy`)
SELECT `command_id`, `text`, `required_level`, `reward_xp`, `cost_happiness`, `cost_energy`
FROM `pet_commands_data`;
DROP TABLE `pet_commands_data`;
RENAME TABLE `pet_commands_data_new` TO `pet_commands_data`;
ALTER TABLE `calendar_rewards`
MODIFY `product_name` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
MODIFY `custom_image` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
MODIFY `badge` VARCHAR(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
MODIFY `subscription_type` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '';
@@ -0,0 +1,158 @@
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_time_more_than' WHERE `public_name` = 'wf_cnd_time_more_than';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_time_less_than' WHERE `public_name` = 'wf_cnd_time_less_than';
UPDATE `items_base` SET `interaction_type` = 'wf_act_give_reward' WHERE `public_name` = 'wf_act_give_reward';
UPDATE `items_base` SET `interaction_type` = 'wf_act_call_stacks' WHERE `public_name` = 'wf_act_call_stacks';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_maze';
UPDATE `items_base` SET `interaction_type` = 'wf_act_give_score_tm' WHERE `public_name` = 'wf_act_give_score_tm';
UPDATE `items_base` SET `interaction_type` = 'wf_act_move_to_dir' WHERE `public_name` = 'wf_act_move_to_dir';
UPDATE `items_base` SET `interaction_type` = 'wf_act_leave_team' WHERE `public_name` = 'wf_act_leave_team';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_actor_in_team' WHERE `public_name` = 'wf_cnd_actor_in_team';
UPDATE `items_base` SET `interaction_type` = 'wf_act_flee' WHERE `public_name` = 'wf_act_flee';
UPDATE `items_base` SET `interaction_type` = 'wf_act_join_team' WHERE `public_name` = 'wf_act_join_team';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_in_team' WHERE `public_name` = 'wf_cnd_not_in_team';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_furni_on' WHERE `public_name` = 'wf_cnd_not_furni_on';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_stuff_is' WHERE `public_name` = 'wf_cnd_stuff_is';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_stuff_is' WHERE `public_name` = 'wf_cnd_not_stuff_is';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_date_rng_active' WHERE `public_name` = 'wf_cnd_date_rng_active';
UPDATE `items_base` SET `interaction_type` = 'wf_act_bot_clothes' WHERE `public_name` = 'wf_act_bot_clothes';
UPDATE `items_base` SET `interaction_type` = 'wf_act_bot_teleport' WHERE `public_name` = 'wf_act_bot_teleport';
UPDATE `items_base` SET `interaction_type` = 'wf_act_bot_follow_avatar' WHERE `public_name` = 'wf_act_bot_follow_avatar';
UPDATE `items_base` SET `interaction_type` = 'wf_act_bot_give_handitem' WHERE `public_name` = 'wf_act_bot_give_handitem';
UPDATE `items_base` SET `interaction_type` = 'wf_act_bot_move' WHERE `public_name` = 'wf_act_bot_move';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_has_handitem' WHERE `public_name` = 'wf_cnd_has_handitem';
UPDATE `items_base` SET `interaction_type` = 'wf_act_bot_talk_to_avatar' WHERE `public_name` = 'wf_act_bot_talk_to_avatar';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_bot_reached_avtr' WHERE `public_name` = 'wf_trg_bot_reached_avtr';
UPDATE `items_base` SET `interaction_type` = 'wf_act_bot_talk' WHERE `public_name` = 'wf_act_bot_talk';
UPDATE `items_base` SET `interaction_type` = 'wf_act_move_rotate' WHERE `public_name` = 'wf_act_move_rotate';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_wire2';
UPDATE `items_base` SET `interaction_type` = 'switch' WHERE `public_name` = 'wf_floor_switch2';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_state_changed' WHERE `public_name` = 'wf_trg_state_changed';
UPDATE `items_base` SET `interaction_type` = 'wf_xtra_random' WHERE `public_name` = 'wf_xtra_random';
UPDATE `items_base` SET `interaction_type` = 'wf_xtra_unseen' WHERE `public_name` = 'wf_xtra_unseen';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_periodically' WHERE `public_name` = 'wf_trg_periodically';
UPDATE `items_base` SET `interaction_type` = 'pyramid' WHERE `public_name` = 'wf_pyramid';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_score_achieved' WHERE `public_name` = 'wf_trg_score_achieved';
UPDATE `items_base` SET `interaction_type` = 'wf_act_teleport_to' WHERE `public_name` = 'wf_act_teleport_to';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_says_something' WHERE `public_name` = 'wf_trg_says_something';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_wire4';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_walks_off_furni' WHERE `public_name` = 'wf_trg_walks_off_furni';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_at_given_time' WHERE `public_name` = 'wf_trg_at_given_time';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_game_ends' WHERE `public_name` = 'wf_trg_game_ends';
UPDATE `items_base` SET `interaction_type` = 'wf_act_show_message' WHERE `public_name` = 'wf_act_show_message';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_collision' WHERE `public_name` = 'wf_trg_collision';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_enter_room' WHERE `public_name` = 'wf_trg_enter_room';
UPDATE `items_base` SET `interaction_type` = 'wf_act_toggle_state' WHERE `public_name` = 'wf_act_toggle_state';
UPDATE `items_base` SET `interaction_type` = 'gate' WHERE `public_name` = 'wf_firegate';
UPDATE `items_base` SET `interaction_type` = 'pressureplate' WHERE `public_name` = 'wf_ringplate';
UPDATE `items_base` SET `interaction_type` = 'pressureplate' WHERE `public_name` = 'wf_pressureplate';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_glowball';
UPDATE `items_base` SET `interaction_type` = 'wf_act_reset_timers' WHERE `public_name` = 'wf_act_reset_timers';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_furnis_hv_avtrs' WHERE `public_name` = 'wf_cnd_furnis_hv_avtrs';
UPDATE `items_base` SET `interaction_type` = 'pressureplate' WHERE `public_name` = 'wf_arrowplate';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_trggrer_on_frn' WHERE `public_name` = 'wf_cnd_trggrer_on_frn';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_wire1';
UPDATE `items_base` SET `interaction_type` = 'wf_act_give_score' WHERE `public_name` = 'wf_act_give_score';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_wire3';
UPDATE `items_base` SET `interaction_type` = 'gate' WHERE `public_name` = 'wf_glassdoor';
UPDATE `items_base` SET `interaction_type` = 'wf_act_match_to_sshot' WHERE `public_name` = 'wf_act_match_to_sshot';
UPDATE `items_base` SET `interaction_type` = 'switch' WHERE `public_name` = 'wf_floor_switch1';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_game_starts' WHERE `public_name` = 'wf_trg_game_starts';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_walks_on_furni' WHERE `public_name` = 'wf_trg_walks_on_furni';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_actor_in_group' WHERE `public_name` = 'wf_cnd_actor_in_group';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_in_group' WHERE `public_name` = 'wf_cnd_not_in_group';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_trggrer_on' WHERE `public_name` = 'wf_cnd_not_trggrer_on';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_hv_avtrs' WHERE `public_name` = 'wf_cnd_not_hv_avtrs';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_user_count_in' WHERE `public_name` = 'wf_cnd_user_count_in';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_user_count' WHERE `public_name` = 'wf_cnd_not_user_count';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_wearing_effect' WHERE `public_name` = 'wf_cnd_wearing_effect';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_wearing_fx' WHERE `public_name` = 'wf_cnd_not_wearing_fx';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_wearing_badge' WHERE `public_name` = 'wf_cnd_wearing_badge';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_wearing_b' WHERE `public_name` = 'wf_cnd_not_wearing_b';
UPDATE `items_base` SET `interaction_type` = 'wf_act_kick_user' WHERE `public_name` = 'wf_act_kick_user';
UPDATE `items_base` SET `interaction_type` = 'wf_act_mute_triggerer' WHERE `public_name` = 'wf_act_mute_triggerer';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_match_snapshot' WHERE `public_name` = 'wf_cnd_match_snapshot';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_match_snap' WHERE `public_name` = 'wf_cnd_not_match_snap';
UPDATE `items_base` SET `interaction_type` = 'wf_blob' WHERE `public_name` = 'wf_blob';
UPDATE `items_base` SET `interaction_type` = 'wf_blob' WHERE `public_name` = 'wf_blob2';
UPDATE `items_base` SET `interaction_type` = 'puzzle_box' WHERE `public_name` = 'wf_box';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_has_furni_on' WHERE `public_name` = 'wf_cnd_has_furni_on';
UPDATE `items_base` SET `interaction_type` = 'wf_act_super_wired' WHERE `public_name` = 'wf_act_super_wired';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_super_wired' WHERE `public_name` = 'wf_cnd_super_wired';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_period_long' WHERE `public_name` = 'wf_trg_period_long';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_bot_reached_stf' WHERE `public_name` = 'wf_trg_bot_reached_stf';
UPDATE `items_base` SET `interaction_type` = 'wf_act_chase' WHERE `public_name` = 'wf_act_chase';
UPDATE `items_base` SET `interaction_type` = 'wf_act_move_furni_to' WHERE `public_name` = 'wf_act_move_furni_to';
UPDATE `items_base` SET `interaction_type` = 'wf_act_toggle_to_rnd' WHERE `public_name` = 'wf_act_toggle_to_rnd';
UPDATE `items_base` SET `interaction_type` = 'wf_blob' WHERE `public_name` = 'wf_blob2_vis';
UPDATE `items_base` SET `interaction_type` = 'wf_blob' WHERE `public_name` = 'wf_blob_invis';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_at_time_long' WHERE `public_name` = 'wf_trg_at_time_long';
UPDATE `items_base` SET `interaction_type` = 'wf_act_control_clock' WHERE `public_name` = 'wf_act_control_clock';
UPDATE `items_base` SET `interaction_type` = 'game_upcounter' WHERE `public_name` = 'wf_game_upcounter1';
UPDATE `items_base` SET `interaction_type` = 'game_upcounter' WHERE `public_name` = 'wf_game_upcounter2';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_clock_counter' WHERE `public_name` = 'wf_trg_clock_counter';
UPDATE `items_base` SET `interaction_type` = 'wf_xtra_or_eval' WHERE `public_name` = 'wf_xtra_or_eval';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_test_act';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_test_cnd';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_test_trg';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_test_xtra';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_counter_time_matches' WHERE `public_name` = 'wf_cnd_counter_time_matches';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_match_date' WHERE `public_name` = 'wf_cnd_match_date';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_match_time' WHERE `public_name` = 'wf_cnd_match_time';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_has_handitem' WHERE `public_name` = 'wf_cnd_not_has_handitem';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_triggerer_match' WHERE `public_name` = 'wf_cnd_not_triggerer_match';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_not_user_performs_action' WHERE `public_name` = 'wf_cnd_not_user_performs_action';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_team_has_rank' WHERE `public_name` = 'wf_cnd_team_has_rank';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_team_has_score' WHERE `public_name` = 'wf_cnd_team_has_score';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_triggerer_match' WHERE `public_name` = 'wf_cnd_triggerer_match';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_user_performs_action' WHERE `public_name` = 'wf_cnd_user_performs_action';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_user_performs_action' WHERE `public_name` = 'wf_trg_user_performs_action';
UPDATE `items_base` SET `interaction_type` = 'wf_act_freeze' WHERE `public_name` = 'wf_act_freeze';
UPDATE `items_base` SET `interaction_type` = 'wf_act_furni_to_furni' WHERE `public_name` = 'wf_act_furni_to_furni';
UPDATE `items_base` SET `interaction_type` = 'wf_act_furni_to_user' WHERE `public_name` = 'wf_act_furni_to_user';
UPDATE `items_base` SET `interaction_type` = 'wf_act_rel_mov' WHERE `public_name` = 'wf_act_rel_mov';
UPDATE `items_base` SET `interaction_type` = 'wf_act_send_signal' WHERE `public_name` = 'wf_act_send_signal';
UPDATE `items_base` SET `interaction_type` = 'wf_act_set_altitude' WHERE `public_name` = 'wf_act_set_altitude';
UPDATE `items_base` SET `interaction_type` = 'wf_act_unfreeze' WHERE `public_name` = 'wf_act_unfreeze';
UPDATE `items_base` SET `interaction_type` = 'antenna' WHERE `public_name` = 'wf_antenna1';
UPDATE `items_base` SET `interaction_type` = 'antenna' WHERE `public_name` = 'wf_antenna2';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_actor_dir' WHERE `public_name` = 'wf_cnd_actor_dir';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_has_altitude' WHERE `public_name` = 'wf_cnd_has_altitude';
UPDATE `items_base` SET `interaction_type` = 'wf_cnd_slc_quantity' WHERE `public_name` = 'wf_cnd_slc_quantity';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_numbertile1';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_numbertile2';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_furni_altitude' WHERE `public_name` = 'wf_slc_furni_altitude';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_furni_area' WHERE `public_name` = 'wf_slc_furni_area';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_furni_bytype' WHERE `public_name` = 'wf_slc_furni_bytype';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_furni_neighborhood' WHERE `public_name` = 'wf_slc_furni_neighborhood';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_furni_onfurni' WHERE `public_name` = 'wf_slc_furni_onfurni';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_furni_picks' WHERE `public_name` = 'wf_slc_furni_picks';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_furni_signal' WHERE `public_name` = 'wf_slc_furni_signal';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_area' WHERE `public_name` = 'wf_slc_users_area';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_byaction' WHERE `public_name` = 'wf_slc_users_byaction';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_byname' WHERE `public_name` = 'wf_slc_users_byname';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_bytype' WHERE `public_name` = 'wf_slc_users_bytype';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_group' WHERE `public_name` = 'wf_slc_users_group';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_handitem' WHERE `public_name` = 'wf_slc_users_handitem';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_neighborhood' WHERE `public_name` = 'wf_slc_users_neighborhood';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_onfurni' WHERE `public_name` = 'wf_slc_users_onfurni';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_signal' WHERE `public_name` = 'wf_slc_users_signal';
UPDATE `items_base` SET `interaction_type` = 'wf_slc_users_team' WHERE `public_name` = 'wf_slc_users_team';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_test_slc';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_tile1';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_tile2';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_click_furni' WHERE `public_name` = 'wf_trg_click_furni';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_period_short' WHERE `public_name` = 'wf_trg_period_short';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_recv_signal' WHERE `public_name` = 'wf_trg_recv_signal';
UPDATE `items_base` SET `interaction_type` = 'wf_trg_stuff_state' WHERE `public_name` = 'wf_trg_stuff_state';
UPDATE `items_base` SET `interaction_type` = 'wf_xtra_anim_time' WHERE `public_name` = 'wf_xtra_anim_time';
UPDATE `items_base` SET `interaction_type` = 'wf_xtra_execution_limit' WHERE `public_name` = 'wf_xtra_execution_limit';
UPDATE `items_base` SET `interaction_type` = 'wf_xtra_filter_furni' WHERE `public_name` = 'wf_xtra_filter_furni';
UPDATE `items_base` SET `interaction_type` = 'wf_xtra_filter_users' WHERE `public_name` = 'wf_xtra_filter_users';
UPDATE `items_base` SET `interaction_type` = 'wf_xtra_mov_carry_users' WHERE `public_name` = 'wf_xtra_mov_carry_users';
UPDATE `items_base` SET `interaction_type` = 'wf_xtra_mov_no_animation' WHERE `public_name` = 'wf_xtra_mov_no_animation';
UPDATE `items_base` SET `interaction_type` = 'wf_xtra_mov_physics' WHERE `public_name` = 'wf_xtra_mov_physics';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_act_log';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_act_neg_log';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_var_echo';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_xtra_var_lvlup_system';
UPDATE `items_base` SET `interaction_type` = 'default' WHERE `public_name` = 'wf_xtra_var_time_util';
@@ -90,6 +90,7 @@ public class InteractionWiredHighscore extends HabboItem {
try {
int state = Integer.parseInt(this.getExtradata());
this.setExtradata(Math.abs(state - 1) + "");
this.needsUpdate(true);
room.updateItem(this);
} catch (Exception e) {
LOGGER.error("Caught exception", e);
@@ -150,4 +151,4 @@ public class InteractionWiredHighscore extends HabboItem {
public void reloadData() {
this.data = Emulator.getGameEnvironment().getItemManager().getHighscoreManager().getHighscoreRowsForItem(this.getId(), this.clearType, this.scoreType);
}
}
}
@@ -27,8 +27,8 @@ public class BuildersClubRoomSupport {
private static final Logger LOGGER = LoggerFactory.getLogger(BuildersClubRoomSupport.class);
public static final int DEFAULT_TRIAL_FURNI_LIMIT = 50;
// Uses the built-in system account row so Builders Club furni have a valid foreign-key owner in `items`,
// while still being treated as virtual / non-user-owned everywhere else in the BC flow.
// Runtime-only owner marker used to display Builders Club furni as virtual/non-user-owned in-room.
// The actual DB owner for persistence/FK purposes is tracked separately on the item instance.
public static final int VIRTUAL_OWNER_ID = 1;
public static final String DISPLAY_OWNER_NAME = "Builders Club";
@@ -628,7 +628,7 @@ public class RoomItemManager {
}
if (BuildersClubRoomSupport.isTrackedItem(item.getId()) && item.getUserId() != BuildersClubRoomSupport.VIRTUAL_OWNER_ID) {
item.setUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
item.needsUpdate(true);
}
@@ -45,6 +45,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
private int id;
private int userId;
private int databaseUserId;
private int roomId;
private Item baseItem;
private String wallPosition;
@@ -62,6 +63,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
public HabboItem(ResultSet set, Item baseItem) throws SQLException {
this.id = set.getInt("id");
this.userId = set.getInt("user_id");
this.databaseUserId = this.userId;
this.roomId = set.getInt("room_id");
this.baseItem = baseItem;
this.wallPosition = set.getString("wall_pos");
@@ -81,6 +83,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
public HabboItem(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
this.id = id;
this.userId = userId;
this.databaseUserId = userId;
this.roomId = 0;
this.baseItem = item;
this.wallPosition = "";
@@ -169,6 +172,11 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
public void setUserId(int userId) {
this.userId = userId;
this.databaseUserId = userId;
}
public void setVirtualUserId(int userId) {
this.userId = userId;
}
public int getRoomId() {
@@ -275,7 +283,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
}
} else if (this.needsUpdate) {
try (PreparedStatement statement = connection.prepareStatement("UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?")) {
statement.setInt(1, this.userId);
statement.setInt(1, this.databaseUserId);
statement.setInt(2, this.roomId);
statement.setString(3, this.wallPosition);
statement.setInt(4, this.x);
@@ -198,7 +198,7 @@ public class HabboManager {
public ArrayList<HabboInfo> getCloneAccounts(Habbo habbo, int limit) {
ArrayList<HabboInfo> habboInfo = new ArrayList<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE ip_register = ? OR ip_current = ? AND id != ? ORDER BY id DESC LIMIT ?")) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE (ip_register = ? OR ip_current = ?) AND id != ? ORDER BY id DESC LIMIT ?")) {
statement.setString(1, habbo.getHabboInfo().getIpRegister());
statement.setString(2, habbo.getHabboInfo().getIpLogin());
statement.setInt(3, habbo.getHabboInfo().getId());
@@ -42,7 +42,7 @@ import java.util.concurrent.ConcurrentHashMap;
* It receives {@link WiredEvent} objects, finds matching stacks via {@link WiredStackIndex},
* evaluates conditions, and executes effects.
* </p>
*
*
* <h3>Execution Flow:</h3>
* <ol>
* <li>Receive event via {@link #handleEvent(WiredEvent)}</li>
@@ -52,14 +52,14 @@ import java.util.concurrent.ConcurrentHashMap;
* <li>Execute effects (respecting random/unseen modifiers)</li>
* <li>Handle delays for timed effects</li>
* </ol>
*
*
* <h3>Safety Features:</h3>
* <ul>
* <li>Step limits via {@link WiredState} prevent infinite loops</li>
* <li>Effect cooldowns prevent rapid re-triggering</li>
* <li>Exceptions are caught and logged, not propagated</li>
* </ul>
*
*
* @see WiredEvent
* @see WiredContext
* @see WiredStackIndex
@@ -67,16 +67,16 @@ import java.util.concurrent.ConcurrentHashMap;
public final class WiredEngine {
private static final Logger LOGGER = LoggerFactory.getLogger(WiredEngine.class);
/** Maximum recursion depth to prevent infinite loops (e.g., collision + chase) */
public static int MAX_RECURSION_DEPTH = 10;
/** Maximum events of same type per room within rate limit window before banning */
public static int MAX_EVENTS_PER_WINDOW = 100;
/** Time window for counting rapid events (milliseconds) */
public static long RATE_LIMIT_WINDOW_MS = 10000;
/** Duration to ban wired execution in a room after abuse detected (milliseconds) */
public static long WIRED_BAN_DURATION_MS = 600000;
@@ -110,25 +110,28 @@ public final class WiredEngine {
private final WiredServices services;
private final WiredStackIndex index;
private final int maxStepsPerStack;
/** Track unseen effect indices per room+tile for round-robin selection */
private final ConcurrentHashMap<String, Integer> unseenIndices;
/** Track recursion depth per room to prevent infinite loops */
private final ConcurrentHashMap<Integer, Integer> roomRecursionDepth;
/** Track event timestamps per room+eventType for rate limiting: key = "roomId:eventType" */
private final ConcurrentHashMap<String, EventRateTracker> eventRateLimiters;
/** Track rooms that are banned from wired execution: roomId -> ban expiry timestamp */
private final ConcurrentHashMap<Integer, Long> bannedRooms;
/** Track monitor diagnostics per room */
private final ConcurrentHashMap<Integer, WiredRoomDiagnostics> roomDiagnostics;
/** Cache room+eventType+sourceItemId -> matching stacks for source-triggered timer events */
private final ConcurrentHashMap<String, List<WiredStack>> sourceStacksByTriggerKey;
/**
* Create a new wired engine.
*
*
* @param services the services for performing side effects
* @param index the stack index for finding matching stacks
* @param maxStepsPerStack maximum steps per stack execution (loop protection)
@@ -137,7 +140,7 @@ public final class WiredEngine {
if (services == null) throw new IllegalArgumentException("Services cannot be null");
if (index == null) throw new IllegalArgumentException("Index cannot be null");
if (maxStepsPerStack <= 0) throw new IllegalArgumentException("Max steps must be positive");
this.services = services;
this.index = index;
this.maxStepsPerStack = maxStepsPerStack;
@@ -146,11 +149,12 @@ public final class WiredEngine {
this.eventRateLimiters = new ConcurrentHashMap<>();
this.bannedRooms = new ConcurrentHashMap<>();
this.roomDiagnostics = new ConcurrentHashMap<>();
this.sourceStacksByTriggerKey = new ConcurrentHashMap<>();
}
/**
* Handle a wired event by finding and executing matching stacks.
*
*
* @param event the event to handle
* @return true if any stack was triggered (useful for SAY_SOMETHING to suppress message)
*/
@@ -163,20 +167,14 @@ public final class WiredEngine {
if (room == null || !room.isLoaded()) {
return false;
}
int roomId = room.getId();
// Check if room is banned from wired execution
if (isRoomBanned(roomId)) {
return false;
}
// Check rate limiting to prevent rapid-fire event spam (e.g., collision + chase loop)
// Soft rate limiting to prevent rapid-fire event spam without banning whole rooms
if (isRateLimited(roomId, room, event.getType())) {
// Room has been banned, all events will be dropped
return false;
}
// Check and increment recursion depth to prevent infinite loops
int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0);
if (currentDepth >= MAX_RECURSION_DEPTH) {
@@ -192,7 +190,7 @@ public final class WiredEngine {
return false;
}
roomRecursionDepth.put(roomId, currentDepth + 1);
try {
return handleEventInternal(event, room);
} finally {
@@ -205,7 +203,129 @@ public final class WiredEngine {
}
}
}
/**
* Handle a wired event when the source trigger item is already known.
* This is mainly used by timed wired triggers to avoid scanning unrelated stacks.
*
* @param event the event to handle
* @param sourceItemId the trigger item id that originated the event
* @return true if any matching stack was triggered
*/
public boolean handleEventForSourceItem(WiredEvent event, int sourceItemId) {
if (event == null || sourceItemId <= 0) {
return false;
}
Room room = event.getRoom();
if (room == null || !room.isLoaded()) {
return false;
}
int roomId = room.getId();
if (isRateLimited(roomId, room, event.getType())) {
return false;
}
int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0);
if (currentDepth >= MAX_RECURSION_DEPTH) {
LOGGER.warn("Wired recursion limit reached in room {} (depth: {}). " +
"Possible infinite loop detected (source item execution). Aborting.", roomId, currentDepth);
debug(room, "RECURSION LIMIT REACHED - aborting source-item execution");
return false;
}
roomRecursionDepth.put(roomId, currentDepth + 1);
try {
return handleEventForSourceItemInternal(event, room, sourceItemId);
} finally {
int newDepth = roomRecursionDepth.getOrDefault(roomId, 1) - 1;
if (newDepth <= 0) {
roomRecursionDepth.remove(roomId);
} else {
roomRecursionDepth.put(roomId, newDepth);
}
}
}
/**
* Internal event handling optimized for a known source trigger item.
*/
private boolean handleEventForSourceItemInternal(WiredEvent event, Room room, int sourceItemId) {
List<WiredStack> 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<WiredStack> getStacksForSourceItem(Room room, WiredEvent.Type eventType, int sourceItemId) {
String cacheKey = room.getId() + ":" + eventType.name() + ":" + sourceItemId;
List<WiredStack> cached = sourceStacksByTriggerKey.get(cacheKey);
if (cached != null) {
return cached;
}
List<WiredStack> allStacks = index.getStacks(room, eventType);
if (allStacks.isEmpty()) {
sourceStacksByTriggerKey.put(cacheKey, Collections.emptyList());
return Collections.emptyList();
}
List<WiredStack> matching = new ArrayList<>();
for (WiredStack stack : allStacks) {
if (stack == null || stack.triggerItem() == null) {
continue;
}
if (stack.triggerItem().getId() == sourceItemId) {
matching.add(stack);
}
}
List<WiredStack> result = matching.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(matching);
sourceStacksByTriggerKey.put(cacheKey, result);
return result;
}
/**
* Internal event handling after recursion check.
*/
@@ -284,8 +404,8 @@ public final class WiredEngine {
String monitorSourceLabel = getMonitorSourceLabel(stack.triggerItem(), event);
int monitorSourceId = getMonitorSourceId(stack.triggerItem());
debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})",
event.getType(),
debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})",
event.getType(),
stack.triggerItem() != null ? stack.triggerItem().getId() : "null",
stack.conditions().size(),
stack.effects().size());
@@ -520,11 +640,11 @@ public final class WiredEngine {
*/
private void executeEffects(WiredStack stack, WiredContext ctx, long currentTime) {
List<IWiredEffect> effects = stack.effects();
if (effects.isEmpty()) {
return;
}
// Selectors already executed before conditions; only run regular effects here
List<IWiredEffect> regulars = new ArrayList<>();
for (IWiredEffect e : effects) {
@@ -698,7 +818,7 @@ public final class WiredEngine {
WiredSelectionFilterSupport.applySelectorFilters(room, stack.triggerItem(), ctx);
}
/**
* Schedule a delayed effect execution.
*/
@@ -722,16 +842,16 @@ public final class WiredEngine {
long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger);
Room room = ctx.room();
RoomUnit actor = ctx.actor().orElse(null);
Emulator.getThreading().run(() -> {
if (!room.isLoaded() || room.getHabbos().isEmpty()) {
diagnostics.completeDelayedEvent();
return;
}
try {
effect.execute(ctx);
// Activate box animation after execution
if (effect instanceof InteractionWiredEffect) {
InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect;
@@ -895,14 +1015,14 @@ public final class WiredEngine {
* Get the next unseen index for round-robin selection.
*/
private int getNextUnseenIndex(WiredStack stack, int effectCount) {
String key = stack.triggerItem() != null
String key = stack.triggerItem() != null
? String.valueOf(stack.triggerItem().getId())
: "default";
int current = unseenIndices.getOrDefault(key, -1);
int next = (current + 1) % effectCount;
unseenIndices.put(key, next);
return next;
}
@@ -915,7 +1035,7 @@ public final class WiredEngine {
// This event is checked for cancellation
THashSet<InteractionWiredEffect> legacyEffects = new THashSet<>();
THashSet<InteractionWiredCondition> legacyConditions = new THashSet<>();
// Extract effects (all effects should now implement both interfaces)
for (IWiredEffect eff : stack.effects()) {
if (eff instanceof InteractionWiredEffect) {
@@ -927,7 +1047,7 @@ public final class WiredEngine {
legacyConditions.add((InteractionWiredCondition) cond);
}
}
WiredStackTriggeredEvent triggeredEvent = new WiredStackTriggeredEvent(
event.getRoom(),
event.getActor().orElse(null),
@@ -935,7 +1055,7 @@ public final class WiredEngine {
legacyEffects,
legacyConditions
);
return !Emulator.getPluginManager().fireEvent(triggeredEvent).isCancelled();
}
return true;
@@ -948,7 +1068,7 @@ public final class WiredEngine {
if (stack.triggerItem() instanceof InteractionWiredTrigger) {
THashSet<InteractionWiredEffect> legacyEffects = new THashSet<>();
THashSet<InteractionWiredCondition> legacyConditions = new THashSet<>();
for (IWiredEffect eff : stack.effects()) {
if (eff instanceof InteractionWiredEffect) {
legacyEffects.add((InteractionWiredEffect) eff);
@@ -959,7 +1079,7 @@ public final class WiredEngine {
legacyConditions.add((InteractionWiredCondition) cond);
}
}
Emulator.getPluginManager().fireEvent(new WiredStackExecutedEvent(
event.getRoom(),
event.getActor().orElse(null),
@@ -974,10 +1094,16 @@ public final class WiredEngine {
* Log a debug message if debug mode is enabled.
*/
private void debug(Room room, String format, Object... args) {
if (WiredManager.isDebugEnabled()) {
String message = String.format(format.replace("{}", "%s"), args);
LOGGER.info("[WiredEngine][Room {}] {}", room.getId(), message);
if (!WiredManager.isDebugEnabled()) {
return;
}
if (!LOGGER.isDebugEnabled()) {
return;
}
String message = String.format(format.replace("{}", "%s"), args);
LOGGER.debug("[WiredEngine][Room {}] {}", room.getId(), message);
}
/**
@@ -987,10 +1113,10 @@ public final class WiredEngine {
if (triggerItem == null || room.getRoomSpecialTypes() == null) {
return;
}
THashSet<InteractionWiredExtra> extras = room.getRoomSpecialTypes().getExtras(
triggerItem.getX(), triggerItem.getY());
if (extras != null) {
for (InteractionWiredExtra extra : extras) {
extra.activateBox(room, roomUnit, millis);
@@ -1068,7 +1194,7 @@ public final class WiredEngine {
public void clearUnseenCache() {
unseenIndices.clear();
}
/**
* Clear recursion tracking for a specific room.
* Should be called when a room is unloaded.
@@ -1077,14 +1203,14 @@ public final class WiredEngine {
public void clearRoomRecursionDepth(int roomId) {
roomRecursionDepth.remove(roomId);
}
/**
* Clear all recursion tracking.
*/
public void clearAllRecursionDepth() {
roomRecursionDepth.clear();
}
/**
* Get the current recursion depth for a room (for debugging).
* @param roomId the room ID
@@ -1093,7 +1219,7 @@ public final class WiredEngine {
public int getRecursionDepth(int roomId) {
return roomRecursionDepth.getOrDefault(roomId, 0);
}
/**
* Clear rate limiters for a specific room.
* Should be called when a room is unloaded.
@@ -1126,10 +1252,45 @@ public final class WiredEngine {
diagnostics.clearLogs();
}
}
/**
* Clear cached source-stack lookups for a specific room.
* @param roomId the room ID
*/
public void clearRoomSourceStackCache(int roomId) {
String prefix = roomId + ":";
sourceStacksByTriggerKey.keySet().removeIf(key -> key.startsWith(prefix));
}
/**
* Clear all cached source-stack lookups.
*/
public void clearAllSourceStackCache() {
sourceStacksByTriggerKey.clear();
}
/**
* Clear all execution-related caches for a specific room.
* @param roomId the room ID
*/
public void clearRoomExecutionCaches(int roomId) {
clearRoomRecursionDepth(roomId);
clearRoomRateLimiters(roomId);
clearRoomSourceStackCache(roomId);
}
/**
* Clear all execution-related caches.
*/
public void clearAllExecutionCaches() {
clearAllRecursionDepth();
eventRateLimiters.clear();
clearAllSourceStackCache();
clearUnseenCache();
}
/**
* Clear room ban for a specific room.
* Should be called when a room is unloaded.
* @param roomId the room ID
*/
public void clearRoomBan(int roomId) {
@@ -1152,7 +1313,7 @@ public final class WiredEngine {
now
);
}
/**
* Check if a room is currently banned from wired execution.
* @param roomId the room ID
@@ -1163,21 +1324,19 @@ public final class WiredEngine {
if (banExpiry == null) {
return false;
}
if (System.currentTimeMillis() >= banExpiry) {
// Ban expired, remove it
bannedRooms.remove(roomId);
return false;
}
return true;
}
/**
* Ban wired execution in a room for WIRED_BAN_DURATION_MS.
* Sends alerts to all users in the room and a scripter alert to staff.
* Ban wired execution in a room.
* @param roomId the room ID
* @param room the room object (for sending alerts)
* @param room the room object
*/
private void banRoom(int roomId, Room room, WiredEvent.Type eventType, int eventCount) {
long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS;
@@ -1213,19 +1372,19 @@ public final class WiredEngine {
LOGGER.warn("Wired abuse detected in room {} ({}). Owner: {}. Wired banned for {} minutes.",
roomId, room.getName(), room.getOwnerName(), banMinutes);
}
/**
* Check if an event should be rate-limited.
* If rate limit exceeded, bans the room and sends alerts.
* Uses a soft limiter only, without banning rooms.
* @param roomId the room ID
* @param room the room object (for sending alerts if banned)
* @param room the room object
* @param eventType the event type
* @return true if the event should be blocked due to rate limiting
*/
private boolean isRateLimited(int roomId, Room room, WiredEvent.Type eventType) {
String key = roomId + ":" + eventType.name();
long now = System.currentTimeMillis();
EventRateTracker tracker = eventRateLimiters.compute(key, (k, existing) -> {
if (existing == null) {
return new EventRateTracker(now);
@@ -1233,7 +1392,7 @@ public final class WiredEngine {
existing.recordEvent(now);
return existing;
});
boolean limited = tracker.isRateLimited(now);
if (limited && tracker.shouldBan(now)) {
// First time hitting limit in this suppression window - ban the room
@@ -1340,43 +1499,38 @@ public final class WiredEngine {
elapsedMs
);
}
/**
* Tracks event rate for a specific room + event type combination.
*/
private static final class EventRateTracker {
private long windowStart;
private int eventCount;
private boolean banned;
private boolean warned;
EventRateTracker(long now) {
this.windowStart = now;
this.eventCount = 1;
this.banned = false;
this.warned = false;
}
synchronized void recordEvent(long now) {
// Reset window if expired
if (now - windowStart > RATE_LIMIT_WINDOW_MS) {
windowStart = now;
eventCount = 1;
// Don't reset banned here - room ban is checked separately
warned = false;
} else {
eventCount++;
}
}
synchronized boolean isRateLimited(long now) {
return eventCount > MAX_EVENTS_PER_WINDOW;
}
/**
* Check if this is the first time we've hit the limit (to trigger ban).
* Returns true only once per suppression window.
*/
synchronized boolean shouldBan(long now) {
if (eventCount > MAX_EVENTS_PER_WINDOW && !banned) {
banned = true;
if (eventCount > MAX_EVENTS_PER_WINDOW && !warned) {
warned = true;
return true;
}
return false;
@@ -57,6 +57,14 @@ import java.util.ArrayDeque;
* <li>{@code wired.engine.debug} - Verbose logging</li>
* </ul>
*
* <h3>Migration Strategy:</h3>
* <ol>
* <li>Set {@code wired.engine.enabled=true} to run both engines in parallel</li>
* <li>Test thoroughly to ensure identical behavior</li>
* <li>Set {@code wired.engine.exclusive=true} to disable legacy engine</li>
* <li>Full migration complete - WiredManager is now the only wired engine</li>
* </ol>
*
* @see WiredEngine
* @see WiredEvents
*/
@@ -80,10 +88,10 @@ public final class WiredManager {
/** The singleton engine instance */
private static volatile WiredEngine engine;
/** The stack index */
private static volatile RoomWiredStackIndex stackIndex;
/** Whether the engine is initialized */
private static volatile boolean initialized = false;
private static final ThreadLocal<Integer> EVENT_HANDLING_DEPTH = new ThreadLocal<>();
@@ -116,7 +124,7 @@ public final class WiredManager {
boolean exclusive = Emulator.getConfig().getBoolean(CONFIG_EXCLUSIVE, DEFAULT_EXCLUSIVE);
int maxSteps = Emulator.getConfig().getInt(CONFIG_MAX_STEPS, DEFAULT_MAX_STEPS);
boolean debug = Emulator.getConfig().getBoolean(CONFIG_DEBUG, false);
// Load additional configuration
MAXIMUM_FURNI_SELECTION = Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5);
TELEPORT_DELAY = Emulator.getConfig().getInt("wired.effect.teleport.delay", 500);
@@ -130,7 +138,7 @@ public final class WiredManager {
stackIndex = new RoomWiredStackIndex();
WiredServices services = DefaultWiredServices.getInstance();
engine = new WiredEngine(services, stackIndex, maxSteps);
// Start the centralized tick service (50ms interval)
WiredTickService.getInstance().start();
@@ -140,8 +148,8 @@ public final class WiredManager {
LOGGER.warn("wired.engine.enabled / wired.engine.exclusive are now compatibility-only flags. WiredManager runs as the exclusive engine runtime.");
}
LOGGER.info("Wired Manager initialized - exclusive runtime active, maxSteps: {}, debug: {}",
maxSteps, debug);
LOGGER.info("Wired Manager initialized - enabled: {}, exclusive runtime active, maxSteps: {}, debug: {}",
enabled, maxSteps, debug);
}
/**
@@ -154,17 +162,18 @@ public final class WiredManager {
}
LOGGER.info("Shutting down Wired Manager...");
// Stop the tick service first
WiredTickService.getInstance().stop();
if (stackIndex != null) {
stackIndex.clearAll();
}
if (engine != null) {
engine.clearUnseenCache();
engine.clearAllDiagnostics();
engine.clearAllExecutionCaches();
}
initialized = false;
@@ -300,6 +309,18 @@ public final class WiredManager {
return true;
}
/**
* Handle a wired event using the new engine when the source trigger item is already known.
* Used by timed wired to avoid scanning unrelated stacks.
*/
private static boolean handleEventForSourceItem(WiredEvent event, HabboItem sourceItem) {
if (!isEnabled() || engine == null || event == null || sourceItem == null) {
return false;
}
return engine.handleEventForSourceItem(event, sourceItem.getId());
}
/**
* Trigger when a user walks onto furniture.
*/
@@ -307,7 +328,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null || item == null) {
return false;
}
WiredEvent event = WiredEvents.userWalksOn(room, user, item);
return handleEvent(event);
}
@@ -319,7 +340,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null || item == null) {
return false;
}
WiredEvent event = WiredEvents.userWalksOff(room, user, item);
return handleEvent(event);
}
@@ -428,7 +449,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
WiredEvent event = WiredEvents.userEntersRoom(room, user);
return handleEvent(event);
}
@@ -452,7 +473,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || item == null) {
return false;
}
WiredEvent event = WiredEvents.furniStateChanged(room, user, item);
return handleEvent(event);
}
@@ -491,24 +512,24 @@ public final class WiredManager {
* Trigger a timer tick.
*/
public static boolean triggerTimerTick(Room room, HabboItem timerItem) {
if (!isEnabled() || room == null) {
if (!isEnabled() || room == null || timerItem == null) {
return false;
}
WiredEvent event = WiredEvents.timerTick(room, timerItem);
return handleEvent(event);
return handleEventForSourceItem(event, timerItem);
}
/**
* Trigger a periodic timer.
*/
public static boolean triggerTimerRepeat(Room room, HabboItem timerItem) {
if (!isEnabled() || room == null) {
if (!isEnabled() || room == null || timerItem == null) {
return false;
}
WiredEvent event = WiredEvents.timerRepeat(room, timerItem);
return handleEvent(event);
return handleEventForSourceItem(event, timerItem);
}
public static boolean triggerClockCounter(Room room, HabboItem counterItem) {
@@ -517,31 +538,31 @@ public final class WiredManager {
}
WiredEvent event = WiredEvents.clockCounter(room, counterItem);
return handleEvent(event);
return handleEventForSourceItem(event, counterItem);
}
/**
* Trigger a long periodic timer.
*/
public static boolean triggerTimerRepeatLong(Room room, HabboItem timerItem) {
if (!isEnabled() || room == null) {
if (!isEnabled() || room == null || timerItem == null) {
return false;
}
WiredEvent event = WiredEvents.timerRepeatLong(room, timerItem);
return handleEvent(event);
return handleEventForSourceItem(event, timerItem);
}
/**
* Trigger a short periodic timer.
*/
public static boolean triggerTimerRepeatShort(Room room, HabboItem timerItem) {
if (!isEnabled() || room == null) {
if (!isEnabled() || room == null || timerItem == null) {
return false;
}
WiredEvent event = WiredEvents.timerRepeatShort(room, timerItem);
return handleEvent(event);
return handleEventForSourceItem(event, timerItem);
}
/**
@@ -551,7 +572,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
WiredEvent event = WiredEvents.gameStarts(room);
return handleEvent(event);
}
@@ -563,7 +584,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
WiredEvent event = WiredEvents.gameEnds(room);
return handleEvent(event);
}
@@ -575,7 +596,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || botUnit == null) {
return false;
}
WiredEvent event = WiredEvents.botCollision(room, botUnit);
return handleEvent(event);
}
@@ -587,7 +608,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || botUnit == null) {
return false;
}
WiredEvent event = WiredEvents.botReachedFurni(room, botUnit, item);
return handleEvent(event);
}
@@ -599,7 +620,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || botUnit == null) {
return false;
}
WiredEvent event = WiredEvents.botReachedHabbo(room, botUnit, targetUser);
return handleEvent(event);
}
@@ -615,7 +636,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
WiredEvent event = WiredEvents.scoreAchieved(room, user, score, scoreAdded);
return handleEvent(event);
}
@@ -627,7 +648,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
WiredEvent event = WiredEvents.userIdles(room, user);
return handleEvent(event);
}
@@ -639,7 +660,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
WiredEvent event = WiredEvents.userUnidles(room, user);
return handleEvent(event);
}
@@ -651,7 +672,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
WiredEvent event = WiredEvents.userStartsDancing(room, user);
return handleEvent(event);
}
@@ -663,7 +684,7 @@ public final class WiredManager {
if (!isEnabled() || room == null || user == null) {
return false;
}
WiredEvent event = WiredEvents.userStopsDancing(room, user);
return handleEvent(event);
}
@@ -675,7 +696,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
WiredEvent event = WiredEvents.teamWins(room, user);
return handleEvent(event);
}
@@ -687,7 +708,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
WiredEvent event = WiredEvents.teamLoses(room, user);
return handleEvent(event);
}
@@ -700,7 +721,7 @@ public final class WiredManager {
if (!isEnabled() || room == null) {
return false;
}
WiredEvent event = WiredEvents.fromLegacy(triggerType, room, roomUnit, stuff);
return handleEvent(event);
}
@@ -712,11 +733,20 @@ public final class WiredManager {
* Call this when wired items are added/removed/moved.
*/
public static void invalidateRoom(Room room) {
if (stackIndex != null && room != null) {
if (room == null) {
return;
}
if (stackIndex != null) {
stackIndex.invalidateAll(room);
if (debugEnabled) {
LOGGER.info("[Wired] Cache invalidated for room {}", room.getId());
}
}
if (engine != null) {
engine.clearRoomExecutionCaches(room.getId());
}
if (debugEnabled) {
LOGGER.info("[Wired] Cache invalidated for room {}", room.getId());
}
}
@@ -727,13 +757,25 @@ public final class WiredManager {
if (stackIndex != null && room != null && tile != null) {
stackIndex.invalidate(room, tile);
}
if (engine != null && room != null) {
engine.clearRoomSourceStackCache(room.getId());
}
}
/**
* Rebuild the wired index for a room.
*/
public static void rebuildRoom(Room room) {
if (stackIndex != null && room != null) {
if (room == null) {
return;
}
if (engine != null) {
engine.clearRoomExecutionCaches(room.getId());
}
if (stackIndex != null) {
stackIndex.rebuild(room);
}
}
@@ -742,19 +784,19 @@ public final class WiredManager {
/** Maximum number of furniture items that can be selected in a single wired component */
public static int MAXIMUM_FURNI_SELECTION = 5;
/** Delay in milliseconds between teleport executions */
public static int TELEPORT_DELAY = 500;
// ========== Debug Mode ==========
/** Debug mode - when enabled, logs detailed wired execution flow */
private static boolean debugEnabled = false;
/**
* Enables or disables wired debug mode.
* When enabled, detailed execution logs are written to help troubleshoot wired stacks.
*
*
* @param enabled true to enable debug logging, false to disable
*/
public static void setDebugEnabled(boolean enabled) {
@@ -763,19 +805,19 @@ public final class WiredManager {
LOGGER.info("Wired debug mode ENABLED");
}
}
/**
* Checks if wired debug mode is enabled.
*
*
* @return true if debug mode is active
*/
public static boolean isDebugEnabled() {
return debugEnabled;
}
/**
* Logs a debug message if debug mode is enabled.
*
*
* @param message the message to log
* @param args optional format arguments
*/
@@ -786,7 +828,7 @@ public final class WiredManager {
}
// ========== JSON Utilities ==========
private static GsonBuilder gsonBuilder = null;
private static Gson cachedGson = null;
@@ -796,12 +838,12 @@ public final class WiredManager {
}
return gsonBuilder;
}
/**
* Gets a cached Gson instance. This is more efficient than calling
* getGsonBuilder().create() multiple times, as Gson instances are thread-safe
* and can be reused.
*
*
* @return a cached Gson instance
*/
public static Gson getGson() {
@@ -812,55 +854,58 @@ public final class WiredManager {
}
// ========== Tick Service Integration ==========
/**
* Registers a tickable wired item with the centralized tick service.
* <p>
* Call this when a time-based wired trigger is placed in a room or when
* a room is loaded.
* </p>
*
*
* @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.
* <p>
* Call this when a time-based wired trigger is picked up or when
* a room is unloaded.
* </p>
*
*
* @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.
* <p>
* Call this when a room is unloaded to clean up all tick registrations.
* </p>
*
*
* @param room the room
*/
public static void unregisterRoomTickables(Room room) {
WiredTickService.getInstance().unregisterRoom(room);
if (room != null) {
room.getFurniVariableManager().clearTransientAssignments();
room.getRoomVariableManager().clearTransientAssignments();
}
if (engine != null && room != null) {
engine.clearRoomExecutionCaches(room.getId());
}
}
/**
* Gets the tick service instance.
*
*
* @return the WiredTickService
*/
public static WiredTickService getTickService() {
@@ -902,7 +947,7 @@ public final class WiredManager {
* <p>
* This uses the new tick service for managing timer resets.
* </p>
*
*
* @param room the room
*/
public static void resetTimers(Room room) {
@@ -935,9 +980,9 @@ public final class WiredManager {
if (item instanceof InteractionWiredEffect && !(item instanceof WiredEffectTriggerStacks)) {
InteractionWiredEffect effect = (InteractionWiredEffect) item;
WiredEvent event = WiredEvent.builder(WiredEvent.Type.CUSTOM, room)
.actor(roomUnit)
.callStackDepth(callStackDepth)
.build();
.actor(roomUnit)
.callStackDepth(callStackDepth)
.build();
WiredContext ctx = new WiredContext(event, effect, DefaultWiredServices.getInstance(), new WiredState(100));
effect.execute(ctx);
effect.setCooldown(millis);
@@ -954,12 +999,12 @@ public final class WiredManager {
/**
* Asynchronously drops/deletes all rewards given by a specific wired item.
* Used when a wired reward box is picked up or reset.
*
*
* @param wiredId The ID of the wired item whose rewards should be deleted
*/
public static void dropRewards(int wiredId) {
Emulator.getThreading().run(() -> {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM wired_rewards_given WHERE wired_item = ?")) {
statement.setInt(1, wiredId);
statement.execute();
@@ -1197,4 +1242,3 @@ public final class WiredManager {
return false;
}
}
@@ -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.
* <p>
* 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:
* </p>
*
* <ul>
* <li>Higher resolution timing (50ms vs 500ms)</li>
* <li>Centralized management - single thread for all rooms</li>
* <li>Proper room lifecycle handling</li>
* <li>Efficient registration/unregistration</li>
* </ul>
*
* <h3>Architecture:</h3>
* <pre>
* WiredTickService (singleton)
* └── ScheduledExecutorService (50ms tick)
* └── For each room with tickables:
* └── For each WiredTickable:
* └── onWiredTick(room, currentTime)
* </pre>
*
* <h3>Thread Safety:</h3>
* All collections are thread-safe. The tick loop catches and logs exceptions
* to prevent one bad item from crashing the entire service.
*
* @see WiredTickable
*
* <p>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.</p>
*/
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<Integer, Set<WiredTickable>> 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.
* <p>
* Should be called during emulator startup after WiredManager.initialize().
* </p>
*/
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.
* <p>
* Should be called during emulator shutdown.
* </p>
*/
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.
* <p>
* The item will start receiving {@link WiredTickable#onWiredTick} calls
* on the next tick cycle.
* </p>
*
* @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<WiredTickable> tickables = roomTickables.computeIfAbsent(
roomId,
k -> ConcurrentHashMap.newKeySet()
);
Set<WiredTickable> 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<WiredTickable> 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<WiredTickable> 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.
* <p>
* Should be called when a room is unloaded.
* </p>
*
* @param room the room
*/
public void unregisterRoom(Room room) {
if (room == null) {
return;
}
Set<WiredTickable> 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<WiredTickable> 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<WiredTickable> 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.
* <p>
* Called at the configured interval by the scheduler. Processes all registered tickables
* across all rooms.
* </p>
*/
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<Integer, Set<WiredTickable>> entry : roomTickables.entrySet()) {
int roomId = entry.getKey();
Set<WiredTickable> tickables = entry.getValue();
if (tickables.isEmpty()) {
if (getShardIndex(roomId) != shardIndex) {
continue;
}
// Get the room - skip if not loaded
Set<WiredTickable> 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);
}
}
@@ -76,13 +76,15 @@ public class BuildersClubPlaceRoomItemEvent extends MessageHandler {
return;
}
HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(BuildersClubRoomSupport.VIRTUAL_OWNER_ID, baseItem, 0, 0, (extraData != null && !extraData.isEmpty()) ? extraData : catalogItem.getExtradata());
HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(placementUserId, baseItem, 0, 0, (extraData != null && !extraData.isEmpty()) ? extraData : catalogItem.getExtradata());
if (item == null) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode));
return;
}
item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
FurnitureMovementError error = room.canPlaceFurnitureAt(item, this.client.getHabbo(), tile, rotation);
if (!error.equals(FurnitureMovementError.NONE)) {
@@ -66,13 +66,15 @@ public class BuildersClubPlaceWallItemEvent extends MessageHandler {
return;
}
HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(BuildersClubRoomSupport.VIRTUAL_OWNER_ID, baseItem, 0, 0, (extraData != null && !extraData.isEmpty()) ? extraData : catalogItem.getExtradata());
HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(placementUserId, baseItem, 0, 0, (extraData != null && !extraData.isEmpty()) ? extraData : catalogItem.getExtradata());
if (item == null) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode));
return;
}
item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
FurnitureMovementError error = room.placeWallFurniAt(item, wallPosition, this.client.getHabbo());
if (!error.equals(FurnitureMovementError.NONE)) {
@@ -44,15 +44,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
@Override
public void handle() throws Exception {
LOGGER.error("DEBUG GIFT: entered CatalogBuyItemAsGiftEvent.handle()");
if (Emulator.getIntUnixTimestamp() - this.client.getHabbo().getHabboStats().lastGiftTimestamp >= CatalogManager.PURCHASE_COOLDOWN) {
this.client.getHabbo().getHabboStats().lastGiftTimestamp = Emulator.getIntUnixTimestamp();
if (ShutdownEmulator.timestamp > 0) {
LOGGER.error("DEBUG GIFT: emulator closing");
this.client.sendResponse(new HotelWillCloseInMinutesComposer((ShutdownEmulator.timestamp - Emulator.getIntUnixTimestamp()) / 60));
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
if (this.client.getHabbo().getHabboStats().isPurchasingFurniture) {
LOGGER.error("DEBUG GIFT: isPurchasingFurniture already true");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
} else {
@@ -60,7 +65,6 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
try {
int pageId = this.packet.readInt();
int itemId = this.packet.readInt();
String extraData = this.packet.readString();
@@ -71,14 +75,22 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
int ribbonId = this.packet.readInt();
boolean showName = this.packet.readBoolean();
LOGGER.error(
"DEBUG GIFT: pageId={}, itemId={}, extraData={}, username={}, spriteId={}, color={}, ribbonId={}, showName={}, message={}",
pageId, itemId, extraData, username, spriteId, color, ribbonId, showName, message
);
int userId = 0;
if (!Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId) && !Emulator.getGameEnvironment().getCatalogManager().giftFurnis.containsKey(spriteId)) {
if (!Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId)
&& !Emulator.getGameEnvironment().getCatalogManager().giftFurnis.containsKey(spriteId)) {
LOGGER.error("DEBUG GIFT: invalid spriteId for gift wrapper/furni -> {}", spriteId);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
if (!GiftConfigurationComposer.BOX_TYPES.contains(color) || !GiftConfigurationComposer.RIBBON_TYPES.contains(ribbonId)) {
LOGGER.error("DEBUG GIFT: invalid color/ribbon -> color={}, ribbonId={}", color, ribbonId);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
@@ -89,10 +101,12 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
Integer iItemId = Emulator.getGameEnvironment().getCatalogManager().giftWrappers.get(spriteId);
if (iItemId == null)
if (iItemId == null) {
iItemId = Emulator.getGameEnvironment().getCatalogManager().giftFurnis.get(spriteId);
}
if (iItemId == null) {
LOGGER.error("DEBUG GIFT: iItemId null for spriteId={}", spriteId);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
@@ -100,9 +114,15 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
Item giftItem = Emulator.getGameEnvironment().getItemManager().getItem(iItemId);
if (giftItem == null) {
giftItem = Emulator.getGameEnvironment().getItemManager().getItem((Integer) Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray()[Emulator.getRandom().nextInt(Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size())]);
LOGGER.error("DEBUG GIFT: direct giftItem null, trying random fallback. iItemId={}", iItemId);
giftItem = Emulator.getGameEnvironment().getItemManager().getItem(
(Integer) Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray()[
Emulator.getRandom().nextInt(Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size())
]
);
if (giftItem == null) {
LOGGER.error("DEBUG GIFT: fallback giftItem also null");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
@@ -112,6 +132,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
if (habbo == null) {
LOGGER.error("DEBUG GIFT: target user not online, checking DB -> {}", username);
try (PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE username = ?")) {
statement.setString(1, username);
@@ -128,6 +149,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
if (userId == 0) {
LOGGER.error("DEBUG GIFT: receiver not found -> {}", username);
this.client.sendResponse(new GiftReceiverNotFoundComposer());
return;
}
@@ -135,11 +157,17 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId);
if (page == null) {
LOGGER.error("DEBUG GIFT: page null -> {}", pageId);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
if (page.getRank() > this.client.getHabbo().getHabboInfo().getRank().getId() || !page.isEnabled() || !page.isVisible()) {
LOGGER.error("DEBUG GIFT: page access denied. pageRank={}, userRank={}, enabled={}, visible={}",
page.getRank(),
this.client.getHabbo().getHabboInfo().getRank().getId(),
page.isEnabled(),
page.isVisible());
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
@@ -147,17 +175,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
CatalogItem item = page.getCatalogItem(itemId);
if (item == null) {
LOGGER.error("DEBUG GIFT: catalog item null -> {}", itemId);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
if (item.isClubOnly() && !this.client.getHabbo().getHabboStats().hasActiveClub()) {
LOGGER.error("DEBUG GIFT: item requires club -> itemId={}", itemId);
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.REQUIRES_CLUB));
return;
}
for (Item baseItem : item.getBaseItems()) {
if (!baseItem.allowGift()) {
LOGGER.error("DEBUG GIFT: base item not giftable -> baseItemId={}, name={}", baseItem.getId(), baseItem.getName());
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
@@ -165,6 +196,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
if (item.isLimited()) {
if (item.getLimitedStack() == item.getLimitedSells()) {
LOGGER.error("DEBUG GIFT: LTD sold out -> itemId={}", itemId);
this.client.sendResponse(new AlertLimitedSoldOutComposer());
return;
}
@@ -173,7 +205,14 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
int totalCredits = item.getCredits();
int totalPoints = item.getPoints();
if(totalCredits > this.client.getHabbo().getHabboInfo().getCredits() || totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType())) {
if (totalCredits > this.client.getHabbo().getHabboInfo().getCredits()
|| totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType())) {
LOGGER.error("DEBUG GIFT: not enough currency. creditsNeeded={}, creditsHave={}, pointsNeeded={}, pointsHave={}, pointsType={}",
totalCredits,
this.client.getHabbo().getHabboInfo().getCredits(),
totalPoints,
this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType()),
item.getPointsType());
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
return;
}
@@ -181,23 +220,34 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
CatalogLimitedConfiguration limitedConfiguration = null;
int limitedStack = 0;
int limitedNumber = 0;
if (item.isLimited()) {
if (Emulator.getGameEnvironment().getCatalogManager().getLimitedConfig(item).available() == 0) {
LOGGER.error("DEBUG GIFT: LTD available=0 -> itemId={}", itemId);
this.client.sendResponse(new AlertLimitedSoldOutComposer());
return;
}
// Check daily LTD limits for the buyer (sender of the gift)
if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) {
int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total");
if (this.client.getHabbo().getHabboStats().totalLtds() >= ltdLimit) {
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total").replace("%itemname%", item.getBaseItems().iterator().next().getFullName()).replace("%limit%", ltdLimit + ""));
LOGGER.error("DEBUG GIFT: sender reached daily total LTD limit");
this.client.getHabbo().alert(
Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total")
.replace("%itemname%", item.getBaseItems().iterator().next().getFullName())
.replace("%limit%", ltdLimit + "")
);
return;
}
ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item");
if (this.client.getHabbo().getHabboStats().totalLtds(item.getId()) >= ltdLimit) {
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item").replace("%itemname%", item.getBaseItems().iterator().next().getFullName()).replace("%limit%", ltdLimit + ""));
LOGGER.error("DEBUG GIFT: sender reached daily LTD item limit");
this.client.getHabbo().alert(
Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item")
.replace("%itemname%", item.getBaseItems().iterator().next().getFullName())
.replace("%limit%", ltdLimit + "")
);
return;
}
}
@@ -210,8 +260,6 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
limitedNumber = limitedConfiguration.getNumber();
limitedStack = limitedConfiguration.getTotalSet();
// Log the LTD purchase for daily limits
this.client.getHabbo().getHabboStats().addLtdLog(item.getId(), Emulator.getIntUnixTimestamp());
}
@@ -229,6 +277,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) as c FROM users_badges WHERE user_id = ? AND badge_code LIKE ?")) {
statement.setInt(1, userId);
statement.setString(2, baseItem.getName());
try (ResultSet rSet = statement.executeQuery()) {
if (rSet.next()) {
c = rSet.getInt("c");
@@ -244,17 +293,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
if (badgeFound) {
LOGGER.error("DEBUG GIFT: receiver already has badge");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.ALREADY_HAVE_BADGE));
return;
}
if (item.getAmount() > 1 || item.getBaseItems().size() > 1) {
LOGGER.error("DEBUG GIFT: unsupported multi amount/baseItems. amount={}, baseItems={}", item.getAmount(), item.getBaseItems().size());
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
for (Item baseItem : item.getBaseItems()) {
if (item.getItemAmount(baseItem.getId()) > 1) {
LOGGER.error("DEBUG GIFT: unsupported item amount > 1 for baseItemId={}", baseItem.getId());
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
@@ -278,37 +330,88 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
badgeFound = true;
}
} else if (item.getName().startsWith("rentable_bot_")) {
LOGGER.error("DEBUG GIFT: rentable bot gifts not supported");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
} else if (Item.isPet(baseItem)) {
LOGGER.error("DEBUG GIFT: pet gifts not supported");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
} else {
if (baseItem.getInteractionType().getType() == InteractionTrophy.class || baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class) {
if (baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class && habbo != null && !habbo.getClient().getHabbo().getInventory().getBadgesComponent().hasBadge(extraData)) {
ScripterManager.scripterDetected(habbo.getClient(), Emulator.getTexts().getValue("scripter.warning.catalog.badge_display").replace("%username%", habbo.getClient().getHabbo().getHabboInfo().getUsername()).replace("%badge%", extraData));
if (baseItem.getInteractionType().getType() == InteractionTrophy.class
|| baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class) {
if (baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class
&& habbo != null
&& !habbo.getClient().getHabbo().getInventory().getBadgesComponent().hasBadge(extraData)) {
ScripterManager.scripterDetected(
habbo.getClient(),
Emulator.getTexts().getValue("scripter.warning.catalog.badge_display")
.replace("%username%", habbo.getClient().getHabbo().getHabboInfo().getUsername())
.replace("%badge%", extraData)
);
extraData = "UMAD";
}
extraData = this.client.getHabbo().getHabboInfo().getUsername() + (char) 9 + Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "-" + (Calendar.getInstance().get(Calendar.MONTH) + 1) + "-" + Calendar.getInstance().get(Calendar.YEAR) + (char) 9 + extraData;
extraData = this.client.getHabbo().getHabboInfo().getUsername()
+ (char) 9
+ Calendar.getInstance().get(Calendar.DAY_OF_MONTH)
+ "-"
+ (Calendar.getInstance().get(Calendar.MONTH) + 1)
+ "-"
+ Calendar.getInstance().get(Calendar.YEAR)
+ (char) 9
+ extraData;
}
if (baseItem.getInteractionType().getType() == InteractionTeleport.class || baseItem.getInteractionType().getType() == InteractionTeleportTile.class) {
HabboItem teleportOne = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
HabboItem teleportTwo = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
if (baseItem.getInteractionType().getType() == InteractionTeleport.class
|| baseItem.getInteractionType().getType() == InteractionTeleportTile.class) {
HabboItem teleportOne = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
HabboItem teleportTwo = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
if (teleportOne == null || teleportTwo == null) {
LOGGER.error("DEBUG GIFT: teleport creation failed. baseItemId={}, teleportOneNull={}, teleportTwoNull={}",
baseItem.getId(), teleportOne == null, teleportTwo == null);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
Emulator.getGameEnvironment().getItemManager().insertTeleportPair(teleportOne.getId(), teleportTwo.getId());
itemsList.add(teleportOne);
itemsList.add(teleportTwo);
} else if (baseItem.getInteractionType().getType() == InteractionHopper.class) {
HabboItem hopper = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedNumber, limitedNumber, extraData);
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedNumber, limitedNumber, extraData);
Emulator.getGameEnvironment().getItemManager().insertHopper(hopper);
if (habboItem == null) {
LOGGER.error("DEBUG GIFT: hopper creation failed. baseItemId={}", baseItem.getId());
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
itemsList.add(hopper);
} else if (baseItem.getInteractionType().getType() == InteractionGuildFurni.class || baseItem.getInteractionType().getType() == InteractionGuildGate.class) {
InteractionGuildFurni habboItem = (InteractionGuildFurni) Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
Emulator.getGameEnvironment().getItemManager().insertHopper(habboItem);
itemsList.add(habboItem);
} else if (baseItem.getInteractionType().getType() == InteractionGuildFurni.class
|| baseItem.getInteractionType().getType() == InteractionGuildGate.class) {
HabboItem createdItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
if (createdItem == null) {
LOGGER.error("DEBUG GIFT: guild item creation failed. baseItemId={}", baseItem.getId());
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
if (!(createdItem instanceof InteractionGuildFurni)) {
LOGGER.error("DEBUG GIFT: created guild item has wrong class -> {}", createdItem.getClass().getName());
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
InteractionGuildFurni habboItem = (InteractionGuildFurni) createdItem;
habboItem.setExtradata("");
habboItem.needsUpdate(true);
int guildId;
try {
guildId = Integer.parseInt(extraData);
@@ -317,15 +420,24 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
Emulator.getThreading().run(habboItem);
Emulator.getGameEnvironment().getGuildManager().setGuild(habboItem, guildId);
itemsList.add(habboItem);
} else {
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
if (habboItem == null) {
LOGGER.error("DEBUG GIFT: normal item creation failed. baseItemId={}, baseItemName={}", baseItem.getId(), baseItem.getName());
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
itemsList.add(habboItem);
}
}
} else {
LOGGER.error("DEBUG GIFT: avatar_effect not supported");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
this.client.sendResponse(new GenericAlertComposer(Emulator.getTexts().getValue("error.catalog.buy.not_yet")));
return;
@@ -333,48 +445,85 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
}
StringBuilder giftData = new StringBuilder(itemsList.size() + "\t");
for (HabboItem i : itemsList) {
giftData.append(i.getId()).append("\t");
}
giftData.append(color).append("\t").append(ribbonId).append("\t").append(showName ? "1" : "0").append("\t").append(message.replace("\t", "")).append("\t").append(this.client.getHabbo().getHabboInfo().getUsername()).append("\t").append(this.client.getHabbo().getHabboInfo().getLook());
HabboItem gift = Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, giftData.toString(), 0, 0);
if (gift == null) {
if (itemsList.isEmpty()) {
LOGGER.error("DEBUG GIFT: itemsList empty before giftData");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
StringBuilder giftData = new StringBuilder(itemsList.size() + "\t");
for (HabboItem i : itemsList) {
if (i == null) {
LOGGER.error("DEBUG GIFT: null HabboItem detected inside itemsList");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
giftData.append(i.getId()).append("\t");
}
giftData.append(color)
.append("\t")
.append(ribbonId)
.append("\t")
.append(showName ? "1" : "0")
.append("\t")
.append(message.replace("\t", ""))
.append("\t")
.append(this.client.getHabbo().getHabboInfo().getUsername())
.append("\t")
.append(this.client.getHabbo().getHabboInfo().getLook());
HabboItem gift = Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, giftData.toString(), 0, 0);
if (gift == null) {
LOGGER.error("DEBUG GIFT: createGift returned null");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
// Mark limited items as sold in the database to prevent duplication after catalog reload
if (limitedConfiguration != null) {
for (HabboItem itm : itemsList) {
if (itm == null) {
LOGGER.error("DEBUG GIFT: null item before limitedSold()");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
limitedConfiguration.limitedSold(item.getId(), this.client.getHabbo(), itm);
}
}
if (this.client.getHabbo().getHabboInfo().getId() != userId) {
AchievementManager.progressAchievement(this.client.getHabbo(), Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftGiver"));
AchievementManager.progressAchievement(
this.client.getHabbo(),
Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftGiver")
);
}
if (habbo != null) {
habbo.getClient().sendResponse(new AddHabboItemComposer(gift));
habbo.getClient().getHabbo().getInventory().getItemsComponent().addItem(gift);
habbo.getClient().sendResponse(new InventoryRefreshComposer());
THashMap<String, String> keys = new THashMap<>();
keys.put("display", "BUBBLE");
keys.put("image", "${image.library.url}notifications/gift.gif");
keys.put("message", Emulator.getTexts().getValue("generic.gift.received.anonymous"));
if (showName) {
keys.put("message", Emulator.getTexts().getValue("generic.gift.received").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
keys.put("message", Emulator.getTexts().getValue("generic.gift.received")
.replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
}
habbo.getClient().sendResponse(new BubbleAlertComposer(BubbleAlertKeys.RECEIVED_BADGE.key, keys));
}
if (this.client.getHabbo().getHabboInfo().getId() != userId) {
AchievementManager.progressAchievement(userId, Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftReceiver"));
AchievementManager.progressAchievement(
userId,
Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftReceiver")
);
}
if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) {
@@ -382,6 +531,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
this.client.getHabbo().giveCredits(-totalCredits);
}
}
if (totalPoints > 0) {
if (item.getPointsType() == 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_PIXELS)) {
this.client.getHabbo().givePixels(-totalPoints);
@@ -390,16 +540,18 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
}
}
LOGGER.error("DEBUG GIFT: success sending PurchaseOKComposer");
this.client.sendResponse(new PurchaseOKComposer(item));
} catch (Exception e) {
LOGGER.error("Exception caught", e);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
}
} catch (Exception e) {
LOGGER.error("Exception caught", e);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
} finally {
this.client.getHabbo().getHabboStats().isPurchasingFurniture = false;
}
} else {
LOGGER.error("DEBUG GIFT: cooldown blocked purchase");
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
}
}
}
}
@@ -126,7 +126,7 @@ public class RoomPlaceItemEvent extends MessageHandler {
trackedUserId = this.client.getHabbo().getHabboInfo().getId();
}
item.setUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
BuildersClubRoomSupport.trackPlacedItem(item.getId(), trackedUserId, room.getId());
BuildersClubRoomSupport.SyncResult syncResult = BuildersClubRoomSupport.syncRoom(room);
@@ -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) {