You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
Merge branch 'merge/duckietm-main-20260407'
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
INSERT INTO emulator_settings (`key`, `value`) VALUES ('wired.tick.workers', '6');
|
||||||
@@ -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';
|
||||||
+2
-1
@@ -90,6 +90,7 @@ public class InteractionWiredHighscore extends HabboItem {
|
|||||||
try {
|
try {
|
||||||
int state = Integer.parseInt(this.getExtradata());
|
int state = Integer.parseInt(this.getExtradata());
|
||||||
this.setExtradata(Math.abs(state - 1) + "");
|
this.setExtradata(Math.abs(state - 1) + "");
|
||||||
|
this.needsUpdate(true);
|
||||||
room.updateItem(this);
|
room.updateItem(this);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error("Caught exception", e);
|
LOGGER.error("Caught exception", e);
|
||||||
@@ -150,4 +151,4 @@ public class InteractionWiredHighscore extends HabboItem {
|
|||||||
public void reloadData() {
|
public void reloadData() {
|
||||||
this.data = Emulator.getGameEnvironment().getItemManager().getHighscoreManager().getHighscoreRowsForItem(this.getId(), this.clearType, this.scoreType);
|
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);
|
private static final Logger LOGGER = LoggerFactory.getLogger(BuildersClubRoomSupport.class);
|
||||||
|
|
||||||
public static final int DEFAULT_TRIAL_FURNI_LIMIT = 50;
|
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`,
|
// Runtime-only owner marker used to display Builders Club furni as virtual/non-user-owned in-room.
|
||||||
// while still being treated as virtual / non-user-owned everywhere else in the BC flow.
|
// 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 int VIRTUAL_OWNER_ID = 1;
|
||||||
public static final String DISPLAY_OWNER_NAME = "Builders Club";
|
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) {
|
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);
|
item.needsUpdate(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
|
|||||||
|
|
||||||
private int id;
|
private int id;
|
||||||
private int userId;
|
private int userId;
|
||||||
|
private int databaseUserId;
|
||||||
private int roomId;
|
private int roomId;
|
||||||
private Item baseItem;
|
private Item baseItem;
|
||||||
private String wallPosition;
|
private String wallPosition;
|
||||||
@@ -62,6 +63,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
|
|||||||
public HabboItem(ResultSet set, Item baseItem) throws SQLException {
|
public HabboItem(ResultSet set, Item baseItem) throws SQLException {
|
||||||
this.id = set.getInt("id");
|
this.id = set.getInt("id");
|
||||||
this.userId = set.getInt("user_id");
|
this.userId = set.getInt("user_id");
|
||||||
|
this.databaseUserId = this.userId;
|
||||||
this.roomId = set.getInt("room_id");
|
this.roomId = set.getInt("room_id");
|
||||||
this.baseItem = baseItem;
|
this.baseItem = baseItem;
|
||||||
this.wallPosition = set.getString("wall_pos");
|
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) {
|
public HabboItem(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
|
this.databaseUserId = userId;
|
||||||
this.roomId = 0;
|
this.roomId = 0;
|
||||||
this.baseItem = item;
|
this.baseItem = item;
|
||||||
this.wallPosition = "";
|
this.wallPosition = "";
|
||||||
@@ -169,6 +172,11 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
|
|||||||
|
|
||||||
public void setUserId(int userId) {
|
public void setUserId(int userId) {
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
|
this.databaseUserId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVirtualUserId(int userId) {
|
||||||
|
this.userId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getRoomId() {
|
public int getRoomId() {
|
||||||
@@ -275,7 +283,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
|
|||||||
}
|
}
|
||||||
} else if (this.needsUpdate) {
|
} 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 = ?")) {
|
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.setInt(2, this.roomId);
|
||||||
statement.setString(3, this.wallPosition);
|
statement.setString(3, this.wallPosition);
|
||||||
statement.setInt(4, this.x);
|
statement.setInt(4, this.x);
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ public class HabboManager {
|
|||||||
public ArrayList<HabboInfo> getCloneAccounts(Habbo habbo, int limit) {
|
public ArrayList<HabboInfo> getCloneAccounts(Habbo habbo, int limit) {
|
||||||
ArrayList<HabboInfo> habboInfo = new ArrayList<>();
|
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(1, habbo.getHabboInfo().getIpRegister());
|
||||||
statement.setString(2, habbo.getHabboInfo().getIpLogin());
|
statement.setString(2, habbo.getHabboInfo().getIpLogin());
|
||||||
statement.setInt(3, habbo.getHabboInfo().getId());
|
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},
|
* It receives {@link WiredEvent} objects, finds matching stacks via {@link WiredStackIndex},
|
||||||
* evaluates conditions, and executes effects.
|
* evaluates conditions, and executes effects.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* <h3>Execution Flow:</h3>
|
* <h3>Execution Flow:</h3>
|
||||||
* <ol>
|
* <ol>
|
||||||
* <li>Receive event via {@link #handleEvent(WiredEvent)}</li>
|
* <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>Execute effects (respecting random/unseen modifiers)</li>
|
||||||
* <li>Handle delays for timed effects</li>
|
* <li>Handle delays for timed effects</li>
|
||||||
* </ol>
|
* </ol>
|
||||||
*
|
*
|
||||||
* <h3>Safety Features:</h3>
|
* <h3>Safety Features:</h3>
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Step limits via {@link WiredState} prevent infinite loops</li>
|
* <li>Step limits via {@link WiredState} prevent infinite loops</li>
|
||||||
* <li>Effect cooldowns prevent rapid re-triggering</li>
|
* <li>Effect cooldowns prevent rapid re-triggering</li>
|
||||||
* <li>Exceptions are caught and logged, not propagated</li>
|
* <li>Exceptions are caught and logged, not propagated</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @see WiredEvent
|
* @see WiredEvent
|
||||||
* @see WiredContext
|
* @see WiredContext
|
||||||
* @see WiredStackIndex
|
* @see WiredStackIndex
|
||||||
@@ -67,16 +67,16 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
public final class WiredEngine {
|
public final class WiredEngine {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(WiredEngine.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(WiredEngine.class);
|
||||||
|
|
||||||
/** Maximum recursion depth to prevent infinite loops (e.g., collision + chase) */
|
/** Maximum recursion depth to prevent infinite loops (e.g., collision + chase) */
|
||||||
public static int MAX_RECURSION_DEPTH = 10;
|
public static int MAX_RECURSION_DEPTH = 10;
|
||||||
|
|
||||||
/** Maximum events of same type per room within rate limit window before banning */
|
/** Maximum events of same type per room within rate limit window before banning */
|
||||||
public static int MAX_EVENTS_PER_WINDOW = 100;
|
public static int MAX_EVENTS_PER_WINDOW = 100;
|
||||||
|
|
||||||
/** Time window for counting rapid events (milliseconds) */
|
/** Time window for counting rapid events (milliseconds) */
|
||||||
public static long RATE_LIMIT_WINDOW_MS = 10000;
|
public static long RATE_LIMIT_WINDOW_MS = 10000;
|
||||||
|
|
||||||
/** Duration to ban wired execution in a room after abuse detected (milliseconds) */
|
/** Duration to ban wired execution in a room after abuse detected (milliseconds) */
|
||||||
public static long WIRED_BAN_DURATION_MS = 600000;
|
public static long WIRED_BAN_DURATION_MS = 600000;
|
||||||
|
|
||||||
@@ -110,25 +110,28 @@ public final class WiredEngine {
|
|||||||
private final WiredServices services;
|
private final WiredServices services;
|
||||||
private final WiredStackIndex index;
|
private final WiredStackIndex index;
|
||||||
private final int maxStepsPerStack;
|
private final int maxStepsPerStack;
|
||||||
|
|
||||||
/** Track unseen effect indices per room+tile for round-robin selection */
|
/** Track unseen effect indices per room+tile for round-robin selection */
|
||||||
private final ConcurrentHashMap<String, Integer> unseenIndices;
|
private final ConcurrentHashMap<String, Integer> unseenIndices;
|
||||||
|
|
||||||
/** Track recursion depth per room to prevent infinite loops */
|
/** Track recursion depth per room to prevent infinite loops */
|
||||||
private final ConcurrentHashMap<Integer, Integer> roomRecursionDepth;
|
private final ConcurrentHashMap<Integer, Integer> roomRecursionDepth;
|
||||||
|
|
||||||
/** Track event timestamps per room+eventType for rate limiting: key = "roomId:eventType" */
|
/** Track event timestamps per room+eventType for rate limiting: key = "roomId:eventType" */
|
||||||
private final ConcurrentHashMap<String, EventRateTracker> eventRateLimiters;
|
private final ConcurrentHashMap<String, EventRateTracker> eventRateLimiters;
|
||||||
|
|
||||||
/** Track rooms that are banned from wired execution: roomId -> ban expiry timestamp */
|
/** Track rooms that are banned from wired execution: roomId -> ban expiry timestamp */
|
||||||
private final ConcurrentHashMap<Integer, Long> bannedRooms;
|
private final ConcurrentHashMap<Integer, Long> bannedRooms;
|
||||||
|
|
||||||
/** Track monitor diagnostics per room */
|
/** Track monitor diagnostics per room */
|
||||||
private final ConcurrentHashMap<Integer, WiredRoomDiagnostics> roomDiagnostics;
|
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.
|
* Create a new wired engine.
|
||||||
*
|
*
|
||||||
* @param services the services for performing side effects
|
* @param services the services for performing side effects
|
||||||
* @param index the stack index for finding matching stacks
|
* @param index the stack index for finding matching stacks
|
||||||
* @param maxStepsPerStack maximum steps per stack execution (loop protection)
|
* @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 (services == null) throw new IllegalArgumentException("Services cannot be null");
|
||||||
if (index == null) throw new IllegalArgumentException("Index cannot be null");
|
if (index == null) throw new IllegalArgumentException("Index cannot be null");
|
||||||
if (maxStepsPerStack <= 0) throw new IllegalArgumentException("Max steps must be positive");
|
if (maxStepsPerStack <= 0) throw new IllegalArgumentException("Max steps must be positive");
|
||||||
|
|
||||||
this.services = services;
|
this.services = services;
|
||||||
this.index = index;
|
this.index = index;
|
||||||
this.maxStepsPerStack = maxStepsPerStack;
|
this.maxStepsPerStack = maxStepsPerStack;
|
||||||
@@ -146,11 +149,12 @@ public final class WiredEngine {
|
|||||||
this.eventRateLimiters = new ConcurrentHashMap<>();
|
this.eventRateLimiters = new ConcurrentHashMap<>();
|
||||||
this.bannedRooms = new ConcurrentHashMap<>();
|
this.bannedRooms = new ConcurrentHashMap<>();
|
||||||
this.roomDiagnostics = new ConcurrentHashMap<>();
|
this.roomDiagnostics = new ConcurrentHashMap<>();
|
||||||
|
this.sourceStacksByTriggerKey = new ConcurrentHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a wired event by finding and executing matching stacks.
|
* Handle a wired event by finding and executing matching stacks.
|
||||||
*
|
*
|
||||||
* @param event the event to handle
|
* @param event the event to handle
|
||||||
* @return true if any stack was triggered (useful for SAY_SOMETHING to suppress message)
|
* @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()) {
|
if (room == null || !room.isLoaded()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int roomId = room.getId();
|
int roomId = room.getId();
|
||||||
|
|
||||||
// Check if room is banned from wired execution
|
// Soft rate limiting to prevent rapid-fire event spam without banning whole rooms
|
||||||
if (isRoomBanned(roomId)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check rate limiting to prevent rapid-fire event spam (e.g., collision + chase loop)
|
|
||||||
if (isRateLimited(roomId, room, event.getType())) {
|
if (isRateLimited(roomId, room, event.getType())) {
|
||||||
// Room has been banned, all events will be dropped
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check and increment recursion depth to prevent infinite loops
|
// Check and increment recursion depth to prevent infinite loops
|
||||||
int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0);
|
int currentDepth = roomRecursionDepth.getOrDefault(roomId, 0);
|
||||||
if (currentDepth >= MAX_RECURSION_DEPTH) {
|
if (currentDepth >= MAX_RECURSION_DEPTH) {
|
||||||
@@ -192,7 +190,7 @@ public final class WiredEngine {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
roomRecursionDepth.put(roomId, currentDepth + 1);
|
roomRecursionDepth.put(roomId, currentDepth + 1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return handleEventInternal(event, room);
|
return handleEventInternal(event, room);
|
||||||
} finally {
|
} 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.
|
* Internal event handling after recursion check.
|
||||||
*/
|
*/
|
||||||
@@ -284,8 +404,8 @@ public final class WiredEngine {
|
|||||||
String monitorSourceLabel = getMonitorSourceLabel(stack.triggerItem(), event);
|
String monitorSourceLabel = getMonitorSourceLabel(stack.triggerItem(), event);
|
||||||
int monitorSourceId = getMonitorSourceId(stack.triggerItem());
|
int monitorSourceId = getMonitorSourceId(stack.triggerItem());
|
||||||
|
|
||||||
debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})",
|
debug(room, "Trigger matched: {} at item {} (conditions: {}, effects: {})",
|
||||||
event.getType(),
|
event.getType(),
|
||||||
stack.triggerItem() != null ? stack.triggerItem().getId() : "null",
|
stack.triggerItem() != null ? stack.triggerItem().getId() : "null",
|
||||||
stack.conditions().size(),
|
stack.conditions().size(),
|
||||||
stack.effects().size());
|
stack.effects().size());
|
||||||
@@ -520,11 +640,11 @@ public final class WiredEngine {
|
|||||||
*/
|
*/
|
||||||
private void executeEffects(WiredStack stack, WiredContext ctx, long currentTime) {
|
private void executeEffects(WiredStack stack, WiredContext ctx, long currentTime) {
|
||||||
List<IWiredEffect> effects = stack.effects();
|
List<IWiredEffect> effects = stack.effects();
|
||||||
|
|
||||||
if (effects.isEmpty()) {
|
if (effects.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selectors already executed before conditions; only run regular effects here
|
// Selectors already executed before conditions; only run regular effects here
|
||||||
List<IWiredEffect> regulars = new ArrayList<>();
|
List<IWiredEffect> regulars = new ArrayList<>();
|
||||||
for (IWiredEffect e : effects) {
|
for (IWiredEffect e : effects) {
|
||||||
@@ -698,7 +818,7 @@ public final class WiredEngine {
|
|||||||
|
|
||||||
WiredSelectionFilterSupport.applySelectorFilters(room, stack.triggerItem(), ctx);
|
WiredSelectionFilterSupport.applySelectorFilters(room, stack.triggerItem(), ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule a delayed effect execution.
|
* Schedule a delayed effect execution.
|
||||||
*/
|
*/
|
||||||
@@ -722,16 +842,16 @@ public final class WiredEngine {
|
|||||||
long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger);
|
long remainingDelayMs = Math.max(0L, delayMs - elapsedSinceTrigger);
|
||||||
Room room = ctx.room();
|
Room room = ctx.room();
|
||||||
RoomUnit actor = ctx.actor().orElse(null);
|
RoomUnit actor = ctx.actor().orElse(null);
|
||||||
|
|
||||||
Emulator.getThreading().run(() -> {
|
Emulator.getThreading().run(() -> {
|
||||||
if (!room.isLoaded() || room.getHabbos().isEmpty()) {
|
if (!room.isLoaded() || room.getHabbos().isEmpty()) {
|
||||||
diagnostics.completeDelayedEvent();
|
diagnostics.completeDelayedEvent();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
effect.execute(ctx);
|
effect.execute(ctx);
|
||||||
|
|
||||||
// Activate box animation after execution
|
// Activate box animation after execution
|
||||||
if (effect instanceof InteractionWiredEffect) {
|
if (effect instanceof InteractionWiredEffect) {
|
||||||
InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect;
|
InteractionWiredEffect wiredEffect = (InteractionWiredEffect) effect;
|
||||||
@@ -895,14 +1015,14 @@ public final class WiredEngine {
|
|||||||
* Get the next unseen index for round-robin selection.
|
* Get the next unseen index for round-robin selection.
|
||||||
*/
|
*/
|
||||||
private int getNextUnseenIndex(WiredStack stack, int effectCount) {
|
private int getNextUnseenIndex(WiredStack stack, int effectCount) {
|
||||||
String key = stack.triggerItem() != null
|
String key = stack.triggerItem() != null
|
||||||
? String.valueOf(stack.triggerItem().getId())
|
? String.valueOf(stack.triggerItem().getId())
|
||||||
: "default";
|
: "default";
|
||||||
|
|
||||||
int current = unseenIndices.getOrDefault(key, -1);
|
int current = unseenIndices.getOrDefault(key, -1);
|
||||||
int next = (current + 1) % effectCount;
|
int next = (current + 1) % effectCount;
|
||||||
unseenIndices.put(key, next);
|
unseenIndices.put(key, next);
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -915,7 +1035,7 @@ public final class WiredEngine {
|
|||||||
// This event is checked for cancellation
|
// This event is checked for cancellation
|
||||||
THashSet<InteractionWiredEffect> legacyEffects = new THashSet<>();
|
THashSet<InteractionWiredEffect> legacyEffects = new THashSet<>();
|
||||||
THashSet<InteractionWiredCondition> legacyConditions = new THashSet<>();
|
THashSet<InteractionWiredCondition> legacyConditions = new THashSet<>();
|
||||||
|
|
||||||
// Extract effects (all effects should now implement both interfaces)
|
// Extract effects (all effects should now implement both interfaces)
|
||||||
for (IWiredEffect eff : stack.effects()) {
|
for (IWiredEffect eff : stack.effects()) {
|
||||||
if (eff instanceof InteractionWiredEffect) {
|
if (eff instanceof InteractionWiredEffect) {
|
||||||
@@ -927,7 +1047,7 @@ public final class WiredEngine {
|
|||||||
legacyConditions.add((InteractionWiredCondition) cond);
|
legacyConditions.add((InteractionWiredCondition) cond);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredStackTriggeredEvent triggeredEvent = new WiredStackTriggeredEvent(
|
WiredStackTriggeredEvent triggeredEvent = new WiredStackTriggeredEvent(
|
||||||
event.getRoom(),
|
event.getRoom(),
|
||||||
event.getActor().orElse(null),
|
event.getActor().orElse(null),
|
||||||
@@ -935,7 +1055,7 @@ public final class WiredEngine {
|
|||||||
legacyEffects,
|
legacyEffects,
|
||||||
legacyConditions
|
legacyConditions
|
||||||
);
|
);
|
||||||
|
|
||||||
return !Emulator.getPluginManager().fireEvent(triggeredEvent).isCancelled();
|
return !Emulator.getPluginManager().fireEvent(triggeredEvent).isCancelled();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -948,7 +1068,7 @@ public final class WiredEngine {
|
|||||||
if (stack.triggerItem() instanceof InteractionWiredTrigger) {
|
if (stack.triggerItem() instanceof InteractionWiredTrigger) {
|
||||||
THashSet<InteractionWiredEffect> legacyEffects = new THashSet<>();
|
THashSet<InteractionWiredEffect> legacyEffects = new THashSet<>();
|
||||||
THashSet<InteractionWiredCondition> legacyConditions = new THashSet<>();
|
THashSet<InteractionWiredCondition> legacyConditions = new THashSet<>();
|
||||||
|
|
||||||
for (IWiredEffect eff : stack.effects()) {
|
for (IWiredEffect eff : stack.effects()) {
|
||||||
if (eff instanceof InteractionWiredEffect) {
|
if (eff instanceof InteractionWiredEffect) {
|
||||||
legacyEffects.add((InteractionWiredEffect) eff);
|
legacyEffects.add((InteractionWiredEffect) eff);
|
||||||
@@ -959,7 +1079,7 @@ public final class WiredEngine {
|
|||||||
legacyConditions.add((InteractionWiredCondition) cond);
|
legacyConditions.add((InteractionWiredCondition) cond);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Emulator.getPluginManager().fireEvent(new WiredStackExecutedEvent(
|
Emulator.getPluginManager().fireEvent(new WiredStackExecutedEvent(
|
||||||
event.getRoom(),
|
event.getRoom(),
|
||||||
event.getActor().orElse(null),
|
event.getActor().orElse(null),
|
||||||
@@ -974,10 +1094,16 @@ public final class WiredEngine {
|
|||||||
* Log a debug message if debug mode is enabled.
|
* Log a debug message if debug mode is enabled.
|
||||||
*/
|
*/
|
||||||
private void debug(Room room, String format, Object... args) {
|
private void debug(Room room, String format, Object... args) {
|
||||||
if (WiredManager.isDebugEnabled()) {
|
if (!WiredManager.isDebugEnabled()) {
|
||||||
String message = String.format(format.replace("{}", "%s"), args);
|
return;
|
||||||
LOGGER.info("[WiredEngine][Room {}] {}", room.getId(), message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (triggerItem == null || room.getRoomSpecialTypes() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
THashSet<InteractionWiredExtra> extras = room.getRoomSpecialTypes().getExtras(
|
THashSet<InteractionWiredExtra> extras = room.getRoomSpecialTypes().getExtras(
|
||||||
triggerItem.getX(), triggerItem.getY());
|
triggerItem.getX(), triggerItem.getY());
|
||||||
|
|
||||||
if (extras != null) {
|
if (extras != null) {
|
||||||
for (InteractionWiredExtra extra : extras) {
|
for (InteractionWiredExtra extra : extras) {
|
||||||
extra.activateBox(room, roomUnit, millis);
|
extra.activateBox(room, roomUnit, millis);
|
||||||
@@ -1068,7 +1194,7 @@ public final class WiredEngine {
|
|||||||
public void clearUnseenCache() {
|
public void clearUnseenCache() {
|
||||||
unseenIndices.clear();
|
unseenIndices.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear recursion tracking for a specific room.
|
* Clear recursion tracking for a specific room.
|
||||||
* Should be called when a room is unloaded.
|
* Should be called when a room is unloaded.
|
||||||
@@ -1077,14 +1203,14 @@ public final class WiredEngine {
|
|||||||
public void clearRoomRecursionDepth(int roomId) {
|
public void clearRoomRecursionDepth(int roomId) {
|
||||||
roomRecursionDepth.remove(roomId);
|
roomRecursionDepth.remove(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all recursion tracking.
|
* Clear all recursion tracking.
|
||||||
*/
|
*/
|
||||||
public void clearAllRecursionDepth() {
|
public void clearAllRecursionDepth() {
|
||||||
roomRecursionDepth.clear();
|
roomRecursionDepth.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current recursion depth for a room (for debugging).
|
* Get the current recursion depth for a room (for debugging).
|
||||||
* @param roomId the room ID
|
* @param roomId the room ID
|
||||||
@@ -1093,7 +1219,7 @@ public final class WiredEngine {
|
|||||||
public int getRecursionDepth(int roomId) {
|
public int getRecursionDepth(int roomId) {
|
||||||
return roomRecursionDepth.getOrDefault(roomId, 0);
|
return roomRecursionDepth.getOrDefault(roomId, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear rate limiters for a specific room.
|
* Clear rate limiters for a specific room.
|
||||||
* Should be called when a room is unloaded.
|
* Should be called when a room is unloaded.
|
||||||
@@ -1126,10 +1252,45 @@ public final class WiredEngine {
|
|||||||
diagnostics.clearLogs();
|
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.
|
* Clear room ban for a specific room.
|
||||||
* Should be called when a room is unloaded.
|
|
||||||
* @param roomId the room ID
|
* @param roomId the room ID
|
||||||
*/
|
*/
|
||||||
public void clearRoomBan(int roomId) {
|
public void clearRoomBan(int roomId) {
|
||||||
@@ -1152,7 +1313,7 @@ public final class WiredEngine {
|
|||||||
now
|
now
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a room is currently banned from wired execution.
|
* Check if a room is currently banned from wired execution.
|
||||||
* @param roomId the room ID
|
* @param roomId the room ID
|
||||||
@@ -1163,21 +1324,19 @@ public final class WiredEngine {
|
|||||||
if (banExpiry == null) {
|
if (banExpiry == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (System.currentTimeMillis() >= banExpiry) {
|
if (System.currentTimeMillis() >= banExpiry) {
|
||||||
// Ban expired, remove it
|
|
||||||
bannedRooms.remove(roomId);
|
bannedRooms.remove(roomId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ban wired execution in a room for WIRED_BAN_DURATION_MS.
|
* Ban wired execution in a room.
|
||||||
* Sends alerts to all users in the room and a scripter alert to staff.
|
|
||||||
* @param roomId the room ID
|
* @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) {
|
private void banRoom(int roomId, Room room, WiredEvent.Type eventType, int eventCount) {
|
||||||
long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS;
|
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.",
|
LOGGER.warn("Wired abuse detected in room {} ({}). Owner: {}. Wired banned for {} minutes.",
|
||||||
roomId, room.getName(), room.getOwnerName(), banMinutes);
|
roomId, room.getName(), room.getOwnerName(), banMinutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an event should be rate-limited.
|
* 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 roomId the room ID
|
||||||
* @param room the room object (for sending alerts if banned)
|
* @param room the room object
|
||||||
* @param eventType the event type
|
* @param eventType the event type
|
||||||
* @return true if the event should be blocked due to rate limiting
|
* @return true if the event should be blocked due to rate limiting
|
||||||
*/
|
*/
|
||||||
private boolean isRateLimited(int roomId, Room room, WiredEvent.Type eventType) {
|
private boolean isRateLimited(int roomId, Room room, WiredEvent.Type eventType) {
|
||||||
String key = roomId + ":" + eventType.name();
|
String key = roomId + ":" + eventType.name();
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
EventRateTracker tracker = eventRateLimiters.compute(key, (k, existing) -> {
|
EventRateTracker tracker = eventRateLimiters.compute(key, (k, existing) -> {
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
return new EventRateTracker(now);
|
return new EventRateTracker(now);
|
||||||
@@ -1233,7 +1392,7 @@ public final class WiredEngine {
|
|||||||
existing.recordEvent(now);
|
existing.recordEvent(now);
|
||||||
return existing;
|
return existing;
|
||||||
});
|
});
|
||||||
|
|
||||||
boolean limited = tracker.isRateLimited(now);
|
boolean limited = tracker.isRateLimited(now);
|
||||||
if (limited && tracker.shouldBan(now)) {
|
if (limited && tracker.shouldBan(now)) {
|
||||||
// First time hitting limit in this suppression window - ban the room
|
// First time hitting limit in this suppression window - ban the room
|
||||||
@@ -1340,43 +1499,38 @@ public final class WiredEngine {
|
|||||||
elapsedMs
|
elapsedMs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks event rate for a specific room + event type combination.
|
* Tracks event rate for a specific room + event type combination.
|
||||||
*/
|
*/
|
||||||
private static final class EventRateTracker {
|
private static final class EventRateTracker {
|
||||||
private long windowStart;
|
private long windowStart;
|
||||||
private int eventCount;
|
private int eventCount;
|
||||||
private boolean banned;
|
private boolean warned;
|
||||||
|
|
||||||
EventRateTracker(long now) {
|
EventRateTracker(long now) {
|
||||||
this.windowStart = now;
|
this.windowStart = now;
|
||||||
this.eventCount = 1;
|
this.eventCount = 1;
|
||||||
this.banned = false;
|
this.warned = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void recordEvent(long now) {
|
synchronized void recordEvent(long now) {
|
||||||
// Reset window if expired
|
|
||||||
if (now - windowStart > RATE_LIMIT_WINDOW_MS) {
|
if (now - windowStart > RATE_LIMIT_WINDOW_MS) {
|
||||||
windowStart = now;
|
windowStart = now;
|
||||||
eventCount = 1;
|
eventCount = 1;
|
||||||
// Don't reset banned here - room ban is checked separately
|
warned = false;
|
||||||
} else {
|
} else {
|
||||||
eventCount++;
|
eventCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized boolean isRateLimited(long now) {
|
synchronized boolean isRateLimited(long now) {
|
||||||
return eventCount > MAX_EVENTS_PER_WINDOW;
|
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) {
|
synchronized boolean shouldBan(long now) {
|
||||||
if (eventCount > MAX_EVENTS_PER_WINDOW && !banned) {
|
if (eventCount > MAX_EVENTS_PER_WINDOW && !warned) {
|
||||||
banned = true;
|
warned = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ import java.util.ArrayDeque;
|
|||||||
* <li>{@code wired.engine.debug} - Verbose logging</li>
|
* <li>{@code wired.engine.debug} - Verbose logging</li>
|
||||||
* </ul>
|
* </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 WiredEngine
|
||||||
* @see WiredEvents
|
* @see WiredEvents
|
||||||
*/
|
*/
|
||||||
@@ -80,10 +88,10 @@ public final class WiredManager {
|
|||||||
|
|
||||||
/** The singleton engine instance */
|
/** The singleton engine instance */
|
||||||
private static volatile WiredEngine engine;
|
private static volatile WiredEngine engine;
|
||||||
|
|
||||||
/** The stack index */
|
/** The stack index */
|
||||||
private static volatile RoomWiredStackIndex stackIndex;
|
private static volatile RoomWiredStackIndex stackIndex;
|
||||||
|
|
||||||
/** Whether the engine is initialized */
|
/** Whether the engine is initialized */
|
||||||
private static volatile boolean initialized = false;
|
private static volatile boolean initialized = false;
|
||||||
private static final ThreadLocal<Integer> EVENT_HANDLING_DEPTH = new ThreadLocal<>();
|
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);
|
boolean exclusive = Emulator.getConfig().getBoolean(CONFIG_EXCLUSIVE, DEFAULT_EXCLUSIVE);
|
||||||
int maxSteps = Emulator.getConfig().getInt(CONFIG_MAX_STEPS, DEFAULT_MAX_STEPS);
|
int maxSteps = Emulator.getConfig().getInt(CONFIG_MAX_STEPS, DEFAULT_MAX_STEPS);
|
||||||
boolean debug = Emulator.getConfig().getBoolean(CONFIG_DEBUG, false);
|
boolean debug = Emulator.getConfig().getBoolean(CONFIG_DEBUG, false);
|
||||||
|
|
||||||
// Load additional configuration
|
// Load additional configuration
|
||||||
MAXIMUM_FURNI_SELECTION = Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5);
|
MAXIMUM_FURNI_SELECTION = Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5);
|
||||||
TELEPORT_DELAY = Emulator.getConfig().getInt("wired.effect.teleport.delay", 500);
|
TELEPORT_DELAY = Emulator.getConfig().getInt("wired.effect.teleport.delay", 500);
|
||||||
@@ -130,7 +138,7 @@ public final class WiredManager {
|
|||||||
stackIndex = new RoomWiredStackIndex();
|
stackIndex = new RoomWiredStackIndex();
|
||||||
WiredServices services = DefaultWiredServices.getInstance();
|
WiredServices services = DefaultWiredServices.getInstance();
|
||||||
engine = new WiredEngine(services, stackIndex, maxSteps);
|
engine = new WiredEngine(services, stackIndex, maxSteps);
|
||||||
|
|
||||||
// Start the centralized tick service (50ms interval)
|
// Start the centralized tick service (50ms interval)
|
||||||
WiredTickService.getInstance().start();
|
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.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: {}",
|
LOGGER.info("Wired Manager initialized - enabled: {}, exclusive runtime active, maxSteps: {}, debug: {}",
|
||||||
maxSteps, debug);
|
enabled, maxSteps, debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,17 +162,18 @@ public final class WiredManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.info("Shutting down Wired Manager...");
|
LOGGER.info("Shutting down Wired Manager...");
|
||||||
|
|
||||||
// Stop the tick service first
|
// Stop the tick service first
|
||||||
WiredTickService.getInstance().stop();
|
WiredTickService.getInstance().stop();
|
||||||
|
|
||||||
if (stackIndex != null) {
|
if (stackIndex != null) {
|
||||||
stackIndex.clearAll();
|
stackIndex.clearAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (engine != null) {
|
if (engine != null) {
|
||||||
engine.clearUnseenCache();
|
engine.clearUnseenCache();
|
||||||
engine.clearAllDiagnostics();
|
engine.clearAllDiagnostics();
|
||||||
|
engine.clearAllExecutionCaches();
|
||||||
}
|
}
|
||||||
|
|
||||||
initialized = false;
|
initialized = false;
|
||||||
@@ -300,6 +309,18 @@ public final class WiredManager {
|
|||||||
return true;
|
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.
|
* Trigger when a user walks onto furniture.
|
||||||
*/
|
*/
|
||||||
@@ -307,7 +328,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null || user == null || item == null) {
|
if (!isEnabled() || room == null || user == null || item == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.userWalksOn(room, user, item);
|
WiredEvent event = WiredEvents.userWalksOn(room, user, item);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -319,7 +340,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null || user == null || item == null) {
|
if (!isEnabled() || room == null || user == null || item == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.userWalksOff(room, user, item);
|
WiredEvent event = WiredEvents.userWalksOff(room, user, item);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -428,7 +449,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null || user == null) {
|
if (!isEnabled() || room == null || user == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.userEntersRoom(room, user);
|
WiredEvent event = WiredEvents.userEntersRoom(room, user);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -452,7 +473,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null || item == null) {
|
if (!isEnabled() || room == null || item == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.furniStateChanged(room, user, item);
|
WiredEvent event = WiredEvents.furniStateChanged(room, user, item);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -491,24 +512,24 @@ public final class WiredManager {
|
|||||||
* Trigger a timer tick.
|
* Trigger a timer tick.
|
||||||
*/
|
*/
|
||||||
public static boolean triggerTimerTick(Room room, HabboItem timerItem) {
|
public static boolean triggerTimerTick(Room room, HabboItem timerItem) {
|
||||||
if (!isEnabled() || room == null) {
|
if (!isEnabled() || room == null || timerItem == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.timerTick(room, timerItem);
|
WiredEvent event = WiredEvents.timerTick(room, timerItem);
|
||||||
return handleEvent(event);
|
return handleEventForSourceItem(event, timerItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger a periodic timer.
|
* Trigger a periodic timer.
|
||||||
*/
|
*/
|
||||||
public static boolean triggerTimerRepeat(Room room, HabboItem timerItem) {
|
public static boolean triggerTimerRepeat(Room room, HabboItem timerItem) {
|
||||||
if (!isEnabled() || room == null) {
|
if (!isEnabled() || room == null || timerItem == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.timerRepeat(room, timerItem);
|
WiredEvent event = WiredEvents.timerRepeat(room, timerItem);
|
||||||
return handleEvent(event);
|
return handleEventForSourceItem(event, timerItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean triggerClockCounter(Room room, HabboItem counterItem) {
|
public static boolean triggerClockCounter(Room room, HabboItem counterItem) {
|
||||||
@@ -517,31 +538,31 @@ public final class WiredManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.clockCounter(room, counterItem);
|
WiredEvent event = WiredEvents.clockCounter(room, counterItem);
|
||||||
return handleEvent(event);
|
return handleEventForSourceItem(event, counterItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger a long periodic timer.
|
* Trigger a long periodic timer.
|
||||||
*/
|
*/
|
||||||
public static boolean triggerTimerRepeatLong(Room room, HabboItem timerItem) {
|
public static boolean triggerTimerRepeatLong(Room room, HabboItem timerItem) {
|
||||||
if (!isEnabled() || room == null) {
|
if (!isEnabled() || room == null || timerItem == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.timerRepeatLong(room, timerItem);
|
WiredEvent event = WiredEvents.timerRepeatLong(room, timerItem);
|
||||||
return handleEvent(event);
|
return handleEventForSourceItem(event, timerItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger a short periodic timer.
|
* Trigger a short periodic timer.
|
||||||
*/
|
*/
|
||||||
public static boolean triggerTimerRepeatShort(Room room, HabboItem timerItem) {
|
public static boolean triggerTimerRepeatShort(Room room, HabboItem timerItem) {
|
||||||
if (!isEnabled() || room == null) {
|
if (!isEnabled() || room == null || timerItem == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.timerRepeatShort(room, timerItem);
|
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) {
|
if (!isEnabled() || room == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.gameStarts(room);
|
WiredEvent event = WiredEvents.gameStarts(room);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -563,7 +584,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null) {
|
if (!isEnabled() || room == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.gameEnds(room);
|
WiredEvent event = WiredEvents.gameEnds(room);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -575,7 +596,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null || botUnit == null) {
|
if (!isEnabled() || room == null || botUnit == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.botCollision(room, botUnit);
|
WiredEvent event = WiredEvents.botCollision(room, botUnit);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -587,7 +608,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null || botUnit == null) {
|
if (!isEnabled() || room == null || botUnit == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.botReachedFurni(room, botUnit, item);
|
WiredEvent event = WiredEvents.botReachedFurni(room, botUnit, item);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -599,7 +620,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null || botUnit == null) {
|
if (!isEnabled() || room == null || botUnit == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.botReachedHabbo(room, botUnit, targetUser);
|
WiredEvent event = WiredEvents.botReachedHabbo(room, botUnit, targetUser);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -615,7 +636,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null || user == null) {
|
if (!isEnabled() || room == null || user == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.scoreAchieved(room, user, score, scoreAdded);
|
WiredEvent event = WiredEvents.scoreAchieved(room, user, score, scoreAdded);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -627,7 +648,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null || user == null) {
|
if (!isEnabled() || room == null || user == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.userIdles(room, user);
|
WiredEvent event = WiredEvents.userIdles(room, user);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -639,7 +660,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null || user == null) {
|
if (!isEnabled() || room == null || user == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.userUnidles(room, user);
|
WiredEvent event = WiredEvents.userUnidles(room, user);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -651,7 +672,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null || user == null) {
|
if (!isEnabled() || room == null || user == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.userStartsDancing(room, user);
|
WiredEvent event = WiredEvents.userStartsDancing(room, user);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -663,7 +684,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null || user == null) {
|
if (!isEnabled() || room == null || user == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.userStopsDancing(room, user);
|
WiredEvent event = WiredEvents.userStopsDancing(room, user);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -675,7 +696,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null) {
|
if (!isEnabled() || room == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.teamWins(room, user);
|
WiredEvent event = WiredEvents.teamWins(room, user);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -687,7 +708,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null) {
|
if (!isEnabled() || room == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.teamLoses(room, user);
|
WiredEvent event = WiredEvents.teamLoses(room, user);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -700,7 +721,7 @@ public final class WiredManager {
|
|||||||
if (!isEnabled() || room == null) {
|
if (!isEnabled() || room == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WiredEvent event = WiredEvents.fromLegacy(triggerType, room, roomUnit, stuff);
|
WiredEvent event = WiredEvents.fromLegacy(triggerType, room, roomUnit, stuff);
|
||||||
return handleEvent(event);
|
return handleEvent(event);
|
||||||
}
|
}
|
||||||
@@ -712,11 +733,20 @@ public final class WiredManager {
|
|||||||
* Call this when wired items are added/removed/moved.
|
* Call this when wired items are added/removed/moved.
|
||||||
*/
|
*/
|
||||||
public static void invalidateRoom(Room room) {
|
public static void invalidateRoom(Room room) {
|
||||||
if (stackIndex != null && room != null) {
|
if (room == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stackIndex != null) {
|
||||||
stackIndex.invalidateAll(room);
|
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) {
|
if (stackIndex != null && room != null && tile != null) {
|
||||||
stackIndex.invalidate(room, tile);
|
stackIndex.invalidate(room, tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (engine != null && room != null) {
|
||||||
|
engine.clearRoomSourceStackCache(room.getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rebuild the wired index for a room.
|
* Rebuild the wired index for a room.
|
||||||
*/
|
*/
|
||||||
public static void rebuildRoom(Room 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);
|
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 */
|
/** Maximum number of furniture items that can be selected in a single wired component */
|
||||||
public static int MAXIMUM_FURNI_SELECTION = 5;
|
public static int MAXIMUM_FURNI_SELECTION = 5;
|
||||||
|
|
||||||
/** Delay in milliseconds between teleport executions */
|
/** Delay in milliseconds between teleport executions */
|
||||||
public static int TELEPORT_DELAY = 500;
|
public static int TELEPORT_DELAY = 500;
|
||||||
|
|
||||||
// ========== Debug Mode ==========
|
// ========== Debug Mode ==========
|
||||||
|
|
||||||
/** Debug mode - when enabled, logs detailed wired execution flow */
|
/** Debug mode - when enabled, logs detailed wired execution flow */
|
||||||
private static boolean debugEnabled = false;
|
private static boolean debugEnabled = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables or disables wired debug mode.
|
* Enables or disables wired debug mode.
|
||||||
* When enabled, detailed execution logs are written to help troubleshoot wired stacks.
|
* When enabled, detailed execution logs are written to help troubleshoot wired stacks.
|
||||||
*
|
*
|
||||||
* @param enabled true to enable debug logging, false to disable
|
* @param enabled true to enable debug logging, false to disable
|
||||||
*/
|
*/
|
||||||
public static void setDebugEnabled(boolean enabled) {
|
public static void setDebugEnabled(boolean enabled) {
|
||||||
@@ -763,19 +805,19 @@ public final class WiredManager {
|
|||||||
LOGGER.info("Wired debug mode ENABLED");
|
LOGGER.info("Wired debug mode ENABLED");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if wired debug mode is enabled.
|
* Checks if wired debug mode is enabled.
|
||||||
*
|
*
|
||||||
* @return true if debug mode is active
|
* @return true if debug mode is active
|
||||||
*/
|
*/
|
||||||
public static boolean isDebugEnabled() {
|
public static boolean isDebugEnabled() {
|
||||||
return debugEnabled;
|
return debugEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a debug message if debug mode is enabled.
|
* Logs a debug message if debug mode is enabled.
|
||||||
*
|
*
|
||||||
* @param message the message to log
|
* @param message the message to log
|
||||||
* @param args optional format arguments
|
* @param args optional format arguments
|
||||||
*/
|
*/
|
||||||
@@ -786,7 +828,7 @@ public final class WiredManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== JSON Utilities ==========
|
// ========== JSON Utilities ==========
|
||||||
|
|
||||||
private static GsonBuilder gsonBuilder = null;
|
private static GsonBuilder gsonBuilder = null;
|
||||||
private static Gson cachedGson = null;
|
private static Gson cachedGson = null;
|
||||||
|
|
||||||
@@ -796,12 +838,12 @@ public final class WiredManager {
|
|||||||
}
|
}
|
||||||
return gsonBuilder;
|
return gsonBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a cached Gson instance. This is more efficient than calling
|
* Gets a cached Gson instance. This is more efficient than calling
|
||||||
* getGsonBuilder().create() multiple times, as Gson instances are thread-safe
|
* getGsonBuilder().create() multiple times, as Gson instances are thread-safe
|
||||||
* and can be reused.
|
* and can be reused.
|
||||||
*
|
*
|
||||||
* @return a cached Gson instance
|
* @return a cached Gson instance
|
||||||
*/
|
*/
|
||||||
public static Gson getGson() {
|
public static Gson getGson() {
|
||||||
@@ -812,55 +854,58 @@ public final class WiredManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== Tick Service Integration ==========
|
// ========== Tick Service Integration ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a tickable wired item with the centralized tick service.
|
* Registers a tickable wired item with the centralized tick service.
|
||||||
* <p>
|
* <p>
|
||||||
* Call this when a time-based wired trigger is placed in a room or when
|
* Call this when a time-based wired trigger is placed in a room or when
|
||||||
* a room is loaded.
|
* a room is loaded.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param room the room the item is in
|
* @param room the room the item is in
|
||||||
* @param tickable the tickable item (e.g., WiredTriggerRepeater)
|
* @param tickable the tickable item (e.g., WiredTriggerRepeater)
|
||||||
*/
|
*/
|
||||||
public static void registerTickable(Room room, WiredTickable tickable) {
|
public static void registerTickable(Room room, WiredTickable tickable) {
|
||||||
WiredTickService.getInstance().register(room, tickable);
|
WiredTickService.getInstance().register(room, tickable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregisters a tickable wired item from the tick service.
|
* Unregisters a tickable wired item from the tick service.
|
||||||
* <p>
|
* <p>
|
||||||
* Call this when a time-based wired trigger is picked up or when
|
* Call this when a time-based wired trigger is picked up or when
|
||||||
* a room is unloaded.
|
* a room is unloaded.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param room the room the item was in
|
* @param room the room the item was in
|
||||||
* @param tickable the tickable item
|
* @param tickable the tickable item
|
||||||
*/
|
*/
|
||||||
public static void unregisterTickable(Room room, WiredTickable tickable) {
|
public static void unregisterTickable(Room room, WiredTickable tickable) {
|
||||||
WiredTickService.getInstance().unregister(room, tickable);
|
WiredTickService.getInstance().unregister(room, tickable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregisters all tickables for a room.
|
* Unregisters all tickables for a room.
|
||||||
* <p>
|
* <p>
|
||||||
* Call this when a room is unloaded to clean up all tick registrations.
|
* Call this when a room is unloaded to clean up all tick registrations.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param room the room
|
* @param room the room
|
||||||
*/
|
*/
|
||||||
public static void unregisterRoomTickables(Room room) {
|
public static void unregisterRoomTickables(Room room) {
|
||||||
WiredTickService.getInstance().unregisterRoom(room);
|
WiredTickService.getInstance().unregisterRoom(room);
|
||||||
|
|
||||||
if (room != null) {
|
if (room != null) {
|
||||||
room.getFurniVariableManager().clearTransientAssignments();
|
room.getFurniVariableManager().clearTransientAssignments();
|
||||||
room.getRoomVariableManager().clearTransientAssignments();
|
room.getRoomVariableManager().clearTransientAssignments();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (engine != null && room != null) {
|
||||||
|
engine.clearRoomExecutionCaches(room.getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the tick service instance.
|
* Gets the tick service instance.
|
||||||
*
|
*
|
||||||
* @return the WiredTickService
|
* @return the WiredTickService
|
||||||
*/
|
*/
|
||||||
public static WiredTickService getTickService() {
|
public static WiredTickService getTickService() {
|
||||||
@@ -902,7 +947,7 @@ public final class WiredManager {
|
|||||||
* <p>
|
* <p>
|
||||||
* This uses the new tick service for managing timer resets.
|
* This uses the new tick service for managing timer resets.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param room the room
|
* @param room the room
|
||||||
*/
|
*/
|
||||||
public static void resetTimers(Room room) {
|
public static void resetTimers(Room room) {
|
||||||
@@ -935,9 +980,9 @@ public final class WiredManager {
|
|||||||
if (item instanceof InteractionWiredEffect && !(item instanceof WiredEffectTriggerStacks)) {
|
if (item instanceof InteractionWiredEffect && !(item instanceof WiredEffectTriggerStacks)) {
|
||||||
InteractionWiredEffect effect = (InteractionWiredEffect) item;
|
InteractionWiredEffect effect = (InteractionWiredEffect) item;
|
||||||
WiredEvent event = WiredEvent.builder(WiredEvent.Type.CUSTOM, room)
|
WiredEvent event = WiredEvent.builder(WiredEvent.Type.CUSTOM, room)
|
||||||
.actor(roomUnit)
|
.actor(roomUnit)
|
||||||
.callStackDepth(callStackDepth)
|
.callStackDepth(callStackDepth)
|
||||||
.build();
|
.build();
|
||||||
WiredContext ctx = new WiredContext(event, effect, DefaultWiredServices.getInstance(), new WiredState(100));
|
WiredContext ctx = new WiredContext(event, effect, DefaultWiredServices.getInstance(), new WiredState(100));
|
||||||
effect.execute(ctx);
|
effect.execute(ctx);
|
||||||
effect.setCooldown(millis);
|
effect.setCooldown(millis);
|
||||||
@@ -954,12 +999,12 @@ public final class WiredManager {
|
|||||||
/**
|
/**
|
||||||
* Asynchronously drops/deletes all rewards given by a specific wired item.
|
* Asynchronously drops/deletes all rewards given by a specific wired item.
|
||||||
* Used when a wired reward box is picked up or reset.
|
* Used when a wired reward box is picked up or reset.
|
||||||
*
|
*
|
||||||
* @param wiredId The ID of the wired item whose rewards should be deleted
|
* @param wiredId The ID of the wired item whose rewards should be deleted
|
||||||
*/
|
*/
|
||||||
public static void dropRewards(int wiredId) {
|
public static void dropRewards(int wiredId) {
|
||||||
Emulator.getThreading().run(() -> {
|
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 = ?")) {
|
PreparedStatement statement = connection.prepareStatement("DELETE FROM wired_rewards_given WHERE wired_item = ?")) {
|
||||||
statement.setInt(1, wiredId);
|
statement.setInt(1, wiredId);
|
||||||
statement.execute();
|
statement.execute();
|
||||||
@@ -1197,4 +1242,3 @@ public final class WiredManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,133 +9,110 @@ import java.util.Map;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Centralized tick service for all wired timing operations.
|
* Centralized tick service for all wired timing operations.
|
||||||
* <p>
|
*
|
||||||
* This service runs a single 50ms tick loop that processes all registered
|
* <p>This version keeps a single global tick clock, but distributes room processing
|
||||||
* {@link WiredTickable} items across all rooms. This replaces the old
|
* across multiple single-threaded shard workers. A room is always processed on the
|
||||||
* per-room 500ms cycle approach and provides:
|
* same shard, preserving in-room order while preventing one heavy room from delaying
|
||||||
* </p>
|
* all other rooms.</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
|
|
||||||
*/
|
*/
|
||||||
public final class WiredTickService {
|
public final class WiredTickService {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(WiredTickService.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(WiredTickService.class);
|
||||||
|
|
||||||
/** Default tick interval in milliseconds */
|
|
||||||
public static final int DEFAULT_TICK_INTERVAL_MS = 50;
|
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;
|
public static final int MIN_TICK_INTERVAL_MS = 10;
|
||||||
|
|
||||||
/** Maximum allowed tick interval */
|
|
||||||
public static final int MAX_TICK_INTERVAL_MS = 500;
|
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;
|
private static volatile WiredTickService instance;
|
||||||
|
|
||||||
/** The configured tick interval in milliseconds */
|
|
||||||
private int tickIntervalMs = DEFAULT_TICK_INTERVAL_MS;
|
private int tickIntervalMs = DEFAULT_TICK_INTERVAL_MS;
|
||||||
|
|
||||||
/** Whether debug logging is enabled */
|
|
||||||
private boolean debugEnabled = false;
|
private boolean debugEnabled = false;
|
||||||
|
|
||||||
/** Thread priority for the tick service */
|
|
||||||
private int threadPriority = Thread.NORM_PRIORITY + 1;
|
private int threadPriority = Thread.NORM_PRIORITY + 1;
|
||||||
|
private int workerCount = DEFAULT_WORKER_COUNT;
|
||||||
/**
|
|
||||||
* Global tick counter - increments every tick.
|
/** Global logical tick counter shared by every shard. */
|
||||||
* All repeaters use this to stay synchronized.
|
private final AtomicLong tickCount = new AtomicLong(0);
|
||||||
* Repeaters fire when (tickCount * tickIntervalMs) % repeatTime == 0
|
|
||||||
*/
|
/** Schedules the global logical ticks. */
|
||||||
private volatile long tickCount = 0;
|
private ScheduledExecutorService coordinator;
|
||||||
|
|
||||||
/** The scheduled executor for the tick loop */
|
/** One single-thread executor per shard, preserving order inside the shard. */
|
||||||
private ScheduledExecutorService scheduler;
|
private ExecutorService[] shardExecutors;
|
||||||
|
|
||||||
/** The scheduled future for the tick task */
|
/** Highest logical tick requested for each shard. */
|
||||||
private ScheduledFuture<?> tickTask;
|
private AtomicLong[] shardRequestedTicks;
|
||||||
|
|
||||||
/** Map of room ID to set of registered tickables */
|
/** 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;
|
private final ConcurrentHashMap<Integer, Set<WiredTickable>> roomTickables;
|
||||||
|
|
||||||
/** Whether the service is running */
|
|
||||||
private final AtomicBoolean running;
|
private final AtomicBoolean running;
|
||||||
|
|
||||||
/**
|
|
||||||
* Private constructor for singleton.
|
|
||||||
*/
|
|
||||||
private WiredTickService() {
|
private WiredTickService() {
|
||||||
this.roomTickables = new ConcurrentHashMap<>();
|
this.roomTickables = new ConcurrentHashMap<>();
|
||||||
this.running = new AtomicBoolean(false);
|
this.running = new AtomicBoolean(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads configuration from emulator settings.
|
|
||||||
*/
|
|
||||||
private void loadConfiguration() {
|
private void loadConfiguration() {
|
||||||
// Load tick interval
|
|
||||||
int configuredInterval = Emulator.getConfig().getInt("wired.tick.interval.ms", DEFAULT_TICK_INTERVAL_MS);
|
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));
|
this.tickIntervalMs = Math.max(MIN_TICK_INTERVAL_MS, Math.min(MAX_TICK_INTERVAL_MS, configuredInterval));
|
||||||
|
|
||||||
if (configuredInterval != this.tickIntervalMs) {
|
if (configuredInterval != this.tickIntervalMs) {
|
||||||
LOGGER.warn("wired.tick.interval.ms value {} is out of range [{}-{}], using {}",
|
LOGGER.warn(
|
||||||
configuredInterval, MIN_TICK_INTERVAL_MS, MAX_TICK_INTERVAL_MS, this.tickIntervalMs);
|
"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);
|
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);
|
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));
|
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() {
|
public int getTickIntervalMs() {
|
||||||
return tickIntervalMs;
|
return tickIntervalMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if debug logging is enabled.
|
|
||||||
*
|
|
||||||
* @return true if debug is enabled
|
|
||||||
*/
|
|
||||||
public boolean isDebugEnabled() {
|
public boolean isDebugEnabled() {
|
||||||
return debugEnabled;
|
return debugEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public int getWorkerCount() {
|
||||||
* Gets the singleton instance.
|
return workerCount;
|
||||||
*
|
}
|
||||||
* @return the WiredTickService instance
|
|
||||||
*/
|
|
||||||
public static WiredTickService getInstance() {
|
public static WiredTickService getInstance() {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
synchronized (WiredTickService.class) {
|
synchronized (WiredTickService.class) {
|
||||||
@@ -146,150 +123,158 @@ public final class WiredTickService {
|
|||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the tick service.
|
|
||||||
* <p>
|
|
||||||
* Should be called during emulator startup after WiredManager.initialize().
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
public synchronized void start() {
|
public synchronized void start() {
|
||||||
if (running.get()) {
|
if (running.get()) {
|
||||||
LOGGER.warn("WiredTickService already running");
|
LOGGER.warn("WiredTickService already running");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load configuration from emulator settings
|
|
||||||
loadConfiguration();
|
loadConfiguration();
|
||||||
|
|
||||||
LOGGER.info("Starting WiredTickService with {}ms tick interval (debug={}, priority={})...",
|
LOGGER.info(
|
||||||
tickIntervalMs, debugEnabled, threadPriority);
|
"Starting WiredTickService with {}ms tick interval (workers={}, debug={}, priority={})...",
|
||||||
|
tickIntervalMs,
|
||||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
workerCount,
|
||||||
Thread t = new Thread(r, "WiredTickService");
|
debugEnabled,
|
||||||
|
threadPriority
|
||||||
|
);
|
||||||
|
|
||||||
|
this.coordinator = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||||
|
Thread t = new Thread(r, "WiredTickCoordinator");
|
||||||
t.setDaemon(true);
|
t.setDaemon(true);
|
||||||
t.setPriority(threadPriority);
|
t.setPriority(threadPriority);
|
||||||
return t;
|
return t;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.tickTask = scheduler.scheduleAtFixedRate(
|
this.shardExecutors = new ExecutorService[workerCount];
|
||||||
this::tick,
|
this.shardRequestedTicks = new AtomicLong[workerCount];
|
||||||
tickIntervalMs,
|
this.shardProcessedTicks = new AtomicLong[workerCount];
|
||||||
tickIntervalMs,
|
this.shardScheduled = new AtomicBoolean[workerCount];
|
||||||
TimeUnit.MILLISECONDS
|
|
||||||
);
|
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);
|
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");
|
LOGGER.info("WiredTickService started successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the tick service.
|
|
||||||
* <p>
|
|
||||||
* Should be called during emulator shutdown.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
public synchronized void stop() {
|
public synchronized void stop() {
|
||||||
if (!running.get()) {
|
if (!running.get()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.info("Stopping WiredTickService...");
|
LOGGER.info("Stopping WiredTickService...");
|
||||||
|
|
||||||
running.set(false);
|
running.set(false);
|
||||||
|
|
||||||
if (tickTask != null) {
|
if (coordinator != null) {
|
||||||
tickTask.cancel(false);
|
coordinator.shutdown();
|
||||||
tickTask = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scheduler != null) {
|
|
||||||
scheduler.shutdown();
|
|
||||||
try {
|
try {
|
||||||
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
if (!coordinator.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
scheduler.shutdownNow();
|
coordinator.shutdownNow();
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
scheduler.shutdownNow();
|
coordinator.shutdownNow();
|
||||||
Thread.currentThread().interrupt();
|
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();
|
roomTickables.clear();
|
||||||
LOGGER.info("WiredTickService stopped");
|
LOGGER.info("WiredTickService stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the service is running.
|
|
||||||
*
|
|
||||||
* @return true if running
|
|
||||||
*/
|
|
||||||
public boolean isRunning() {
|
public boolean isRunning() {
|
||||||
return running.get();
|
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) {
|
public void register(Room room, WiredTickable tickable) {
|
||||||
if (room == null || tickable == null) {
|
if (room == null || tickable == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int roomId = room.getId();
|
int roomId = room.getId();
|
||||||
Set<WiredTickable> tickables = roomTickables.computeIfAbsent(
|
Set<WiredTickable> tickables = roomTickables.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet());
|
||||||
roomId,
|
|
||||||
k -> ConcurrentHashMap.newKeySet()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tickables.add(tickable)) {
|
if (tickables.add(tickable)) {
|
||||||
tickable.onRegistered(room, System.currentTimeMillis());
|
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) {
|
public void unregister(Room room, WiredTickable tickable) {
|
||||||
if (room == null || tickable == null) {
|
if (room == null || tickable == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int roomId = room.getId();
|
int roomId = room.getId();
|
||||||
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
||||||
|
|
||||||
if (tickables != null) {
|
if (tickables != null) {
|
||||||
if (tickables.remove(tickable)) {
|
if (tickables.remove(tickable)) {
|
||||||
tickable.onUnregistered(room);
|
tickable.onUnregistered(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up empty sets
|
|
||||||
if (tickables.isEmpty()) {
|
if (tickables.isEmpty()) {
|
||||||
roomTickables.remove(roomId);
|
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) {
|
public void unregister(int roomId, int tickableId) {
|
||||||
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
||||||
|
|
||||||
if (tickables != null) {
|
if (tickables != null) {
|
||||||
tickables.removeIf(t -> {
|
tickables.removeIf(t -> {
|
||||||
if (t.getId() == tickableId) {
|
if (t.getId() == tickableId) {
|
||||||
@@ -301,162 +286,240 @@ public final class WiredTickService {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tickables.isEmpty()) {
|
if (tickables.isEmpty()) {
|
||||||
roomTickables.remove(roomId);
|
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) {
|
public void unregisterRoom(Room room) {
|
||||||
if (room == null) {
|
if (room == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<WiredTickable> tickables = roomTickables.remove(room.getId());
|
Set<WiredTickable> tickables = roomTickables.remove(room.getId());
|
||||||
|
|
||||||
if (tickables != null) {
|
if (tickables != null) {
|
||||||
for (WiredTickable tickable : tickables) {
|
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
|
||||||
tickable.onUnregistered(room);
|
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) {
|
public void resetRoomTimers(Room room) {
|
||||||
if (room == null) {
|
if (room == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<WiredTickable> tickables = roomTickables.get(room.getId());
|
Set<WiredTickable> tickables = roomTickables.get(room.getId());
|
||||||
|
|
||||||
if (tickables != null) {
|
if (tickables != null) {
|
||||||
for (WiredTickable tickable : tickables) {
|
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
|
||||||
|
for (WiredTickable tickable : snapshot) {
|
||||||
try {
|
try {
|
||||||
tickable.resetTimer();
|
if (tickable != null) {
|
||||||
} catch (Exception e) {
|
tickable.resetTimer();
|
||||||
LOGGER.error("Error resetting timer for tickable {} in room {}",
|
}
|
||||||
tickable.getId(), room.getId(), e);
|
} 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) {
|
public int getTickableCount(int roomId) {
|
||||||
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
||||||
return tickables != null ? tickables.size() : 0;
|
return tickables != null ? tickables.size() : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the total count of registered tickables across all rooms.
|
|
||||||
*
|
|
||||||
* @return the total count
|
|
||||||
*/
|
|
||||||
public int getTotalTickableCount() {
|
public int getTotalTickableCount() {
|
||||||
return roomTickables.values().stream()
|
return roomTickables.values().stream().mapToInt(Set::size).sum();
|
||||||
.mapToInt(Set::size)
|
|
||||||
.sum();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the count of rooms with registered tickables.
|
|
||||||
*
|
|
||||||
* @return the room count
|
|
||||||
*/
|
|
||||||
public int getActiveRoomCount() {
|
public int getActiveRoomCount() {
|
||||||
return roomTickables.size();
|
return roomTickables.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public long getTickCount() {
|
||||||
* The main tick loop.
|
return tickCount.get();
|
||||||
* <p>
|
}
|
||||||
* Called at the configured interval by the scheduler. Processes all registered tickables
|
|
||||||
* across all rooms.
|
private void dispatchTick() {
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
private void tick() {
|
|
||||||
if (!running.get() || Emulator.isShuttingDown) {
|
if (!running.get() || Emulator.isShuttingDown) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment global tick counter
|
long currentTick = tickCount.incrementAndGet();
|
||||||
tickCount++;
|
|
||||||
|
for (int shardIndex = 0; shardIndex < workerCount; shardIndex++) {
|
||||||
long startTime = System.currentTimeMillis();
|
shardRequestedTicks[shardIndex].set(currentTick);
|
||||||
int tickablesProcessed = 0;
|
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()) {
|
for (Map.Entry<Integer, Set<WiredTickable>> entry : roomTickables.entrySet()) {
|
||||||
int roomId = entry.getKey();
|
int roomId = entry.getKey();
|
||||||
Set<WiredTickable> tickables = entry.getValue();
|
if (getShardIndex(roomId) != shardIndex) {
|
||||||
|
|
||||||
if (tickables.isEmpty()) {
|
|
||||||
continue;
|
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);
|
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
|
||||||
if (room == null || !room.isLoaded()) {
|
if (room == null || !room.isLoaded()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if room is empty (optimization)
|
|
||||||
if (room.getCurrentHabbos().isEmpty() && room.getCurrentBots().isEmpty()) {
|
if (room.getCurrentHabbos().isEmpty() && room.getCurrentBots().isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each tickable
|
long roomStart = System.currentTimeMillis();
|
||||||
for (WiredTickable tickable : tickables) {
|
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 {
|
try {
|
||||||
// Verify item still belongs to this room
|
|
||||||
if (tickable.getRoomId() != roomId) {
|
if (tickable.getRoomId() != roomId) {
|
||||||
// Item moved to another room, unregister it
|
unregister(roomId, tickable.getId());
|
||||||
tickables.remove(tickable);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass global tick count - all tickables see the same counter
|
tickable.onWiredTick(room, currentTick, tickIntervalMs);
|
||||||
// This keeps repeaters with the same interval perfectly synchronized
|
processedTickables++;
|
||||||
tickable.onWiredTick(room, tickCount, tickIntervalMs);
|
|
||||||
tickablesProcessed++;
|
long tickableDuration = System.currentTimeMillis() - tickableStart;
|
||||||
} catch (Exception e) {
|
if (tickableDuration > SLOW_TICKABLE_THRESHOLD_MS) {
|
||||||
LOGGER.error("Error in wired tick for tickable {} in room {}: {}",
|
LOGGER.warn(
|
||||||
tickable.getId(), roomId, e.getMessage(), e);
|
"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
|
long shardDuration = System.currentTimeMillis() - shardStart;
|
||||||
if (debugEnabled && tickablesProcessed > 0) {
|
if (shardDuration > SLOW_SHARD_THRESHOLD_MS) {
|
||||||
LOGGER.debug("Wired tick #{} completed: {} tickables processed in {}ms",
|
LOGGER.warn(
|
||||||
tickCount, tickablesProcessed, System.currentTimeMillis() - startTime);
|
"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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private int getShardIndex(int roomId) {
|
||||||
* Gets the current global tick count.
|
return Math.floorMod(roomId, workerCount);
|
||||||
*
|
|
||||||
* @return the tick count
|
|
||||||
*/
|
|
||||||
public long getTickCount() {
|
|
||||||
return tickCount;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -76,13 +76,15 @@ public class BuildersClubPlaceRoomItemEvent extends MessageHandler {
|
|||||||
return;
|
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) {
|
if (item == null) {
|
||||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode));
|
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
|
||||||
|
|
||||||
FurnitureMovementError error = room.canPlaceFurnitureAt(item, this.client.getHabbo(), tile, rotation);
|
FurnitureMovementError error = room.canPlaceFurnitureAt(item, this.client.getHabbo(), tile, rotation);
|
||||||
|
|
||||||
if (!error.equals(FurnitureMovementError.NONE)) {
|
if (!error.equals(FurnitureMovementError.NONE)) {
|
||||||
|
|||||||
+3
-1
@@ -66,13 +66,15 @@ public class BuildersClubPlaceWallItemEvent extends MessageHandler {
|
|||||||
return;
|
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) {
|
if (item == null) {
|
||||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode));
|
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
|
||||||
|
|
||||||
FurnitureMovementError error = room.placeWallFurniAt(item, wallPosition, this.client.getHabbo());
|
FurnitureMovementError error = room.placeWallFurniAt(item, wallPosition, this.client.getHabbo());
|
||||||
|
|
||||||
if (!error.equals(FurnitureMovementError.NONE)) {
|
if (!error.equals(FurnitureMovementError.NONE)) {
|
||||||
|
|||||||
+194
-42
@@ -44,15 +44,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle() throws Exception {
|
public void handle() throws Exception {
|
||||||
|
LOGGER.error("DEBUG GIFT: entered CatalogBuyItemAsGiftEvent.handle()");
|
||||||
|
|
||||||
if (Emulator.getIntUnixTimestamp() - this.client.getHabbo().getHabboStats().lastGiftTimestamp >= CatalogManager.PURCHASE_COOLDOWN) {
|
if (Emulator.getIntUnixTimestamp() - this.client.getHabbo().getHabboStats().lastGiftTimestamp >= CatalogManager.PURCHASE_COOLDOWN) {
|
||||||
this.client.getHabbo().getHabboStats().lastGiftTimestamp = Emulator.getIntUnixTimestamp();
|
this.client.getHabbo().getHabboStats().lastGiftTimestamp = Emulator.getIntUnixTimestamp();
|
||||||
|
|
||||||
if (ShutdownEmulator.timestamp > 0) {
|
if (ShutdownEmulator.timestamp > 0) {
|
||||||
|
LOGGER.error("DEBUG GIFT: emulator closing");
|
||||||
this.client.sendResponse(new HotelWillCloseInMinutesComposer((ShutdownEmulator.timestamp - Emulator.getIntUnixTimestamp()) / 60));
|
this.client.sendResponse(new HotelWillCloseInMinutesComposer((ShutdownEmulator.timestamp - Emulator.getIntUnixTimestamp()) / 60));
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.client.getHabbo().getHabboStats().isPurchasingFurniture) {
|
if (this.client.getHabbo().getHabboStats().isPurchasingFurniture) {
|
||||||
|
LOGGER.error("DEBUG GIFT: isPurchasingFurniture already true");
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@@ -60,7 +65,6 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
int pageId = this.packet.readInt();
|
int pageId = this.packet.readInt();
|
||||||
int itemId = this.packet.readInt();
|
int itemId = this.packet.readInt();
|
||||||
String extraData = this.packet.readString();
|
String extraData = this.packet.readString();
|
||||||
@@ -71,14 +75,22 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
int ribbonId = this.packet.readInt();
|
int ribbonId = this.packet.readInt();
|
||||||
boolean showName = this.packet.readBoolean();
|
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;
|
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());
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!GiftConfigurationComposer.BOX_TYPES.contains(color) || !GiftConfigurationComposer.RIBBON_TYPES.contains(ribbonId)) {
|
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());
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -89,10 +101,12 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
|
|
||||||
Integer iItemId = Emulator.getGameEnvironment().getCatalogManager().giftWrappers.get(spriteId);
|
Integer iItemId = Emulator.getGameEnvironment().getCatalogManager().giftWrappers.get(spriteId);
|
||||||
|
|
||||||
if (iItemId == null)
|
if (iItemId == null) {
|
||||||
iItemId = Emulator.getGameEnvironment().getCatalogManager().giftFurnis.get(spriteId);
|
iItemId = Emulator.getGameEnvironment().getCatalogManager().giftFurnis.get(spriteId);
|
||||||
|
}
|
||||||
|
|
||||||
if (iItemId == null) {
|
if (iItemId == null) {
|
||||||
|
LOGGER.error("DEBUG GIFT: iItemId null for spriteId={}", spriteId);
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -100,9 +114,15 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
Item giftItem = Emulator.getGameEnvironment().getItemManager().getItem(iItemId);
|
Item giftItem = Emulator.getGameEnvironment().getItemManager().getItem(iItemId);
|
||||||
|
|
||||||
if (giftItem == null) {
|
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) {
|
if (giftItem == null) {
|
||||||
|
LOGGER.error("DEBUG GIFT: fallback giftItem also null");
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -112,6 +132,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
|
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
|
||||||
|
|
||||||
if (habbo == null) {
|
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 = ?")) {
|
try (PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE username = ?")) {
|
||||||
statement.setString(1, username);
|
statement.setString(1, username);
|
||||||
|
|
||||||
@@ -128,6 +149,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (userId == 0) {
|
if (userId == 0) {
|
||||||
|
LOGGER.error("DEBUG GIFT: receiver not found -> {}", username);
|
||||||
this.client.sendResponse(new GiftReceiverNotFoundComposer());
|
this.client.sendResponse(new GiftReceiverNotFoundComposer());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -135,11 +157,17 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId);
|
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId);
|
||||||
|
|
||||||
if (page == null) {
|
if (page == null) {
|
||||||
|
LOGGER.error("DEBUG GIFT: page null -> {}", pageId);
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page.getRank() > this.client.getHabbo().getHabboInfo().getRank().getId() || !page.isEnabled() || !page.isVisible()) {
|
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));
|
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -147,17 +175,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
CatalogItem item = page.getCatalogItem(itemId);
|
CatalogItem item = page.getCatalogItem(itemId);
|
||||||
|
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
|
LOGGER.error("DEBUG GIFT: catalog item null -> {}", itemId);
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.isClubOnly() && !this.client.getHabbo().getHabboStats().hasActiveClub()) {
|
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));
|
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.REQUIRES_CLUB));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Item baseItem : item.getBaseItems()) {
|
for (Item baseItem : item.getBaseItems()) {
|
||||||
if (!baseItem.allowGift()) {
|
if (!baseItem.allowGift()) {
|
||||||
|
LOGGER.error("DEBUG GIFT: base item not giftable -> baseItemId={}, name={}", baseItem.getId(), baseItem.getName());
|
||||||
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
|
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -165,6 +196,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
|
|
||||||
if (item.isLimited()) {
|
if (item.isLimited()) {
|
||||||
if (item.getLimitedStack() == item.getLimitedSells()) {
|
if (item.getLimitedStack() == item.getLimitedSells()) {
|
||||||
|
LOGGER.error("DEBUG GIFT: LTD sold out -> itemId={}", itemId);
|
||||||
this.client.sendResponse(new AlertLimitedSoldOutComposer());
|
this.client.sendResponse(new AlertLimitedSoldOutComposer());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -173,7 +205,14 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
int totalCredits = item.getCredits();
|
int totalCredits = item.getCredits();
|
||||||
int totalPoints = item.getPoints();
|
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));
|
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -181,23 +220,34 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
CatalogLimitedConfiguration limitedConfiguration = null;
|
CatalogLimitedConfiguration limitedConfiguration = null;
|
||||||
int limitedStack = 0;
|
int limitedStack = 0;
|
||||||
int limitedNumber = 0;
|
int limitedNumber = 0;
|
||||||
|
|
||||||
if (item.isLimited()) {
|
if (item.isLimited()) {
|
||||||
if (Emulator.getGameEnvironment().getCatalogManager().getLimitedConfig(item).available() == 0) {
|
if (Emulator.getGameEnvironment().getCatalogManager().getLimitedConfig(item).available() == 0) {
|
||||||
|
LOGGER.error("DEBUG GIFT: LTD available=0 -> itemId={}", itemId);
|
||||||
this.client.sendResponse(new AlertLimitedSoldOutComposer());
|
this.client.sendResponse(new AlertLimitedSoldOutComposer());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check daily LTD limits for the buyer (sender of the gift)
|
|
||||||
if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) {
|
if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) {
|
||||||
int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total");
|
int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total");
|
||||||
if (this.client.getHabbo().getHabboStats().totalLtds() >= ltdLimit) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item");
|
ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item");
|
||||||
if (this.client.getHabbo().getHabboStats().totalLtds(item.getId()) >= ltdLimit) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,8 +260,6 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
|
|
||||||
limitedNumber = limitedConfiguration.getNumber();
|
limitedNumber = limitedConfiguration.getNumber();
|
||||||
limitedStack = limitedConfiguration.getTotalSet();
|
limitedStack = limitedConfiguration.getTotalSet();
|
||||||
|
|
||||||
// Log the LTD purchase for daily limits
|
|
||||||
this.client.getHabbo().getHabboStats().addLtdLog(item.getId(), Emulator.getIntUnixTimestamp());
|
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 ?")) {
|
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) as c FROM users_badges WHERE user_id = ? AND badge_code LIKE ?")) {
|
||||||
statement.setInt(1, userId);
|
statement.setInt(1, userId);
|
||||||
statement.setString(2, baseItem.getName());
|
statement.setString(2, baseItem.getName());
|
||||||
|
|
||||||
try (ResultSet rSet = statement.executeQuery()) {
|
try (ResultSet rSet = statement.executeQuery()) {
|
||||||
if (rSet.next()) {
|
if (rSet.next()) {
|
||||||
c = rSet.getInt("c");
|
c = rSet.getInt("c");
|
||||||
@@ -244,17 +293,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (badgeFound) {
|
if (badgeFound) {
|
||||||
|
LOGGER.error("DEBUG GIFT: receiver already has badge");
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.ALREADY_HAVE_BADGE));
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.ALREADY_HAVE_BADGE));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.getAmount() > 1 || item.getBaseItems().size() > 1) {
|
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());
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Item baseItem : item.getBaseItems()) {
|
for (Item baseItem : item.getBaseItems()) {
|
||||||
if (item.getItemAmount(baseItem.getId()) > 1) {
|
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());
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -278,37 +330,88 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
badgeFound = true;
|
badgeFound = true;
|
||||||
}
|
}
|
||||||
} else if (item.getName().startsWith("rentable_bot_")) {
|
} 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());
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
return;
|
return;
|
||||||
} else if (Item.isPet(baseItem)) {
|
} else if (Item.isPet(baseItem)) {
|
||||||
|
LOGGER.error("DEBUG GIFT: pet gifts not supported");
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
if (baseItem.getInteractionType().getType() == InteractionTrophy.class || baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class) {
|
if (baseItem.getInteractionType().getType() == InteractionTrophy.class
|
||||||
if (baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class && habbo != null && !habbo.getClient().getHabbo().getInventory().getBadgesComponent().hasBadge(extraData)) {
|
|| baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class) {
|
||||||
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() == 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 = "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) {
|
if (baseItem.getInteractionType().getType() == InteractionTeleport.class
|
||||||
HabboItem teleportOne = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
|
|| baseItem.getInteractionType().getType() == InteractionTeleportTile.class) {
|
||||||
HabboItem teleportTwo = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
|
|
||||||
|
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());
|
Emulator.getGameEnvironment().getItemManager().insertTeleportPair(teleportOne.getId(), teleportTwo.getId());
|
||||||
itemsList.add(teleportOne);
|
itemsList.add(teleportOne);
|
||||||
itemsList.add(teleportTwo);
|
itemsList.add(teleportTwo);
|
||||||
|
|
||||||
} else if (baseItem.getInteractionType().getType() == InteractionHopper.class) {
|
} 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);
|
Emulator.getGameEnvironment().getItemManager().insertHopper(habboItem);
|
||||||
} else if (baseItem.getInteractionType().getType() == InteractionGuildFurni.class || baseItem.getInteractionType().getType() == InteractionGuildGate.class) {
|
itemsList.add(habboItem);
|
||||||
InteractionGuildFurni habboItem = (InteractionGuildFurni) Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
|
|
||||||
|
} 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.setExtradata("");
|
||||||
habboItem.needsUpdate(true);
|
habboItem.needsUpdate(true);
|
||||||
|
|
||||||
int guildId;
|
int guildId;
|
||||||
try {
|
try {
|
||||||
guildId = Integer.parseInt(extraData);
|
guildId = Integer.parseInt(extraData);
|
||||||
@@ -317,15 +420,24 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Emulator.getThreading().run(habboItem);
|
Emulator.getThreading().run(habboItem);
|
||||||
Emulator.getGameEnvironment().getGuildManager().setGuild(habboItem, guildId);
|
Emulator.getGameEnvironment().getGuildManager().setGuild(habboItem, guildId);
|
||||||
itemsList.add(habboItem);
|
itemsList.add(habboItem);
|
||||||
} else {
|
} 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);
|
itemsList.add(habboItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
LOGGER.error("DEBUG GIFT: avatar_effect not supported");
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||||
this.client.sendResponse(new GenericAlertComposer(Emulator.getTexts().getValue("error.catalog.buy.not_yet")));
|
this.client.sendResponse(new GenericAlertComposer(Emulator.getTexts().getValue("error.catalog.buy.not_yet")));
|
||||||
return;
|
return;
|
||||||
@@ -333,48 +445,85 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder giftData = new StringBuilder(itemsList.size() + "\t");
|
if (itemsList.isEmpty()) {
|
||||||
|
LOGGER.error("DEBUG GIFT: itemsList empty before giftData");
|
||||||
for (HabboItem i : itemsList) {
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||||
giftData.append(i.getId()).append("\t");
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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());
|
StringBuilder giftData = new StringBuilder(itemsList.size() + "\t");
|
||||||
|
|
||||||
HabboItem gift = Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, giftData.toString(), 0, 0);
|
for (HabboItem i : itemsList) {
|
||||||
|
if (i == null) {
|
||||||
if (gift == 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));
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark limited items as sold in the database to prevent duplication after catalog reload
|
|
||||||
if (limitedConfiguration != null) {
|
if (limitedConfiguration != null) {
|
||||||
for (HabboItem itm : itemsList) {
|
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);
|
limitedConfiguration.limitedSold(item.getId(), this.client.getHabbo(), itm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.client.getHabbo().getHabboInfo().getId() != userId) {
|
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) {
|
if (habbo != null) {
|
||||||
habbo.getClient().sendResponse(new AddHabboItemComposer(gift));
|
habbo.getClient().sendResponse(new AddHabboItemComposer(gift));
|
||||||
habbo.getClient().getHabbo().getInventory().getItemsComponent().addItem(gift);
|
habbo.getClient().getHabbo().getInventory().getItemsComponent().addItem(gift);
|
||||||
habbo.getClient().sendResponse(new InventoryRefreshComposer());
|
habbo.getClient().sendResponse(new InventoryRefreshComposer());
|
||||||
|
|
||||||
THashMap<String, String> keys = new THashMap<>();
|
THashMap<String, String> keys = new THashMap<>();
|
||||||
keys.put("display", "BUBBLE");
|
keys.put("display", "BUBBLE");
|
||||||
keys.put("image", "${image.library.url}notifications/gift.gif");
|
keys.put("image", "${image.library.url}notifications/gift.gif");
|
||||||
keys.put("message", Emulator.getTexts().getValue("generic.gift.received.anonymous"));
|
keys.put("message", Emulator.getTexts().getValue("generic.gift.received.anonymous"));
|
||||||
|
|
||||||
if (showName) {
|
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));
|
habbo.getClient().sendResponse(new BubbleAlertComposer(BubbleAlertKeys.RECEIVED_BADGE.key, keys));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.client.getHabbo().getHabboInfo().getId() != userId) {
|
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)) {
|
if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) {
|
||||||
@@ -382,6 +531,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
|||||||
this.client.getHabbo().giveCredits(-totalCredits);
|
this.client.getHabbo().giveCredits(-totalCredits);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalPoints > 0) {
|
if (totalPoints > 0) {
|
||||||
if (item.getPointsType() == 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_PIXELS)) {
|
if (item.getPointsType() == 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_PIXELS)) {
|
||||||
this.client.getHabbo().givePixels(-totalPoints);
|
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));
|
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 {
|
} finally {
|
||||||
this.client.getHabbo().getHabboStats().isPurchasingFurniture = false;
|
this.client.getHabbo().getHabboStats().isPurchasingFurniture = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
LOGGER.error("DEBUG GIFT: cooldown blocked purchase");
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -126,7 +126,7 @@ public class RoomPlaceItemEvent extends MessageHandler {
|
|||||||
trackedUserId = this.client.getHabbo().getHabboInfo().getId();
|
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.trackPlacedItem(item.getId(), trackedUserId, room.getId());
|
||||||
|
|
||||||
BuildersClubRoomSupport.SyncResult syncResult = BuildersClubRoomSupport.syncRoom(room);
|
BuildersClubRoomSupport.SyncResult syncResult = BuildersClubRoomSupport.syncRoom(room);
|
||||||
|
|||||||
+45
-3
@@ -26,9 +26,16 @@ public class RoomUserWalkEvent extends MessageHandler {
|
|||||||
private static final Logger LOGGER = LoggerFactory.getLogger(RoomUserWalkEvent.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(RoomUserWalkEvent.class);
|
||||||
public static final String CONTROL_KEY = "control";
|
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
|
@Override
|
||||||
public int getRatelimit() {
|
public int getRatelimit() {
|
||||||
return Emulator.getConfig().getInt("pathfinder.click.delay", 0);
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -37,8 +44,43 @@ public class RoomUserWalkEvent extends MessageHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int x = this.packet.readInt(); // Position X
|
int x = this.packet.readInt();
|
||||||
int y = this.packet.readInt(); // Position Y
|
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();
|
Habbo habbo = getControlledHabbo();
|
||||||
if (habbo == null) {
|
if (habbo == null) {
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user