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';
|
||||
+1
@@ -90,6 +90,7 @@ public class InteractionWiredHighscore extends HabboItem {
|
||||
try {
|
||||
int state = Integer.parseInt(this.getExtradata());
|
||||
this.setExtradata(Math.abs(state - 1) + "");
|
||||
this.needsUpdate(true);
|
||||
room.updateItem(this);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Caught exception", e);
|
||||
|
||||
@@ -27,8 +27,8 @@ public class BuildersClubRoomSupport {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(BuildersClubRoomSupport.class);
|
||||
|
||||
public static final int DEFAULT_TRIAL_FURNI_LIMIT = 50;
|
||||
// Uses the built-in system account row so Builders Club furni have a valid foreign-key owner in `items`,
|
||||
// while still being treated as virtual / non-user-owned everywhere else in the BC flow.
|
||||
// Runtime-only owner marker used to display Builders Club furni as virtual/non-user-owned in-room.
|
||||
// The actual DB owner for persistence/FK purposes is tracked separately on the item instance.
|
||||
public static final int VIRTUAL_OWNER_ID = 1;
|
||||
public static final String DISPLAY_OWNER_NAME = "Builders Club";
|
||||
|
||||
|
||||
@@ -628,7 +628,7 @@ public class RoomItemManager {
|
||||
}
|
||||
|
||||
if (BuildersClubRoomSupport.isTrackedItem(item.getId()) && item.getUserId() != BuildersClubRoomSupport.VIRTUAL_OWNER_ID) {
|
||||
item.setUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
|
||||
item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
|
||||
item.needsUpdate(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
|
||||
|
||||
private int id;
|
||||
private int userId;
|
||||
private int databaseUserId;
|
||||
private int roomId;
|
||||
private Item baseItem;
|
||||
private String wallPosition;
|
||||
@@ -62,6 +63,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
|
||||
public HabboItem(ResultSet set, Item baseItem) throws SQLException {
|
||||
this.id = set.getInt("id");
|
||||
this.userId = set.getInt("user_id");
|
||||
this.databaseUserId = this.userId;
|
||||
this.roomId = set.getInt("room_id");
|
||||
this.baseItem = baseItem;
|
||||
this.wallPosition = set.getString("wall_pos");
|
||||
@@ -81,6 +83,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
|
||||
public HabboItem(int id, int userId, Item item, String extradata, int limitedStack, int limitedSells) {
|
||||
this.id = id;
|
||||
this.userId = userId;
|
||||
this.databaseUserId = userId;
|
||||
this.roomId = 0;
|
||||
this.baseItem = item;
|
||||
this.wallPosition = "";
|
||||
@@ -169,6 +172,11 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
|
||||
|
||||
public void setUserId(int userId) {
|
||||
this.userId = userId;
|
||||
this.databaseUserId = userId;
|
||||
}
|
||||
|
||||
public void setVirtualUserId(int userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public int getRoomId() {
|
||||
@@ -275,7 +283,7 @@ public abstract class HabboItem implements Runnable, IEventTriggers {
|
||||
}
|
||||
} else if (this.needsUpdate) {
|
||||
try (PreparedStatement statement = connection.prepareStatement("UPDATE items SET user_id = ?, room_id = ?, wall_pos = ?, x = ?, y = ?, z = ?, rot = ?, extra_data = ?, limited_data = ? WHERE id = ?")) {
|
||||
statement.setInt(1, this.userId);
|
||||
statement.setInt(1, this.databaseUserId);
|
||||
statement.setInt(2, this.roomId);
|
||||
statement.setString(3, this.wallPosition);
|
||||
statement.setInt(4, this.x);
|
||||
|
||||
@@ -198,7 +198,7 @@ public class HabboManager {
|
||||
public ArrayList<HabboInfo> getCloneAccounts(Habbo habbo, int limit) {
|
||||
ArrayList<HabboInfo> habboInfo = new ArrayList<>();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE ip_register = ? OR ip_current = ? AND id != ? ORDER BY id DESC LIMIT ?")) {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE (ip_register = ? OR ip_current = ?) AND id != ? ORDER BY id DESC LIMIT ?")) {
|
||||
statement.setString(1, habbo.getHabboInfo().getIpRegister());
|
||||
statement.setString(2, habbo.getHabboInfo().getIpLogin());
|
||||
statement.setInt(3, habbo.getHabboInfo().getId());
|
||||
|
||||
@@ -126,6 +126,9 @@ public final class WiredEngine {
|
||||
/** Track monitor diagnostics per room */
|
||||
private final ConcurrentHashMap<Integer, WiredRoomDiagnostics> roomDiagnostics;
|
||||
|
||||
/** Cache room+eventType+sourceItemId -> matching stacks for source-triggered timer events */
|
||||
private final ConcurrentHashMap<String, List<WiredStack>> sourceStacksByTriggerKey;
|
||||
|
||||
/**
|
||||
* Create a new wired engine.
|
||||
*
|
||||
@@ -146,6 +149,7 @@ public final class WiredEngine {
|
||||
this.eventRateLimiters = new ConcurrentHashMap<>();
|
||||
this.bannedRooms = new ConcurrentHashMap<>();
|
||||
this.roomDiagnostics = new ConcurrentHashMap<>();
|
||||
this.sourceStacksByTriggerKey = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,14 +170,8 @@ public final class WiredEngine {
|
||||
|
||||
int roomId = room.getId();
|
||||
|
||||
// Check if room is banned from wired execution
|
||||
if (isRoomBanned(roomId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check rate limiting to prevent rapid-fire event spam (e.g., collision + chase loop)
|
||||
// Soft rate limiting to prevent rapid-fire event spam without banning whole rooms
|
||||
if (isRateLimited(roomId, room, event.getType())) {
|
||||
// Room has been banned, all events will be dropped
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -206,6 +204,128 @@ 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.
|
||||
*/
|
||||
@@ -974,10 +1094,16 @@ public final class WiredEngine {
|
||||
* Log a debug message if debug mode is enabled.
|
||||
*/
|
||||
private void debug(Room room, String format, Object... args) {
|
||||
if (WiredManager.isDebugEnabled()) {
|
||||
String message = String.format(format.replace("{}", "%s"), args);
|
||||
LOGGER.info("[WiredEngine][Room {}] {}", room.getId(), message);
|
||||
if (!WiredManager.isDebugEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LOGGER.isDebugEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String message = String.format(format.replace("{}", "%s"), args);
|
||||
LOGGER.debug("[WiredEngine][Room {}] {}", room.getId(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1127,9 +1253,44 @@ public final class WiredEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached source-stack lookups for a specific room.
|
||||
* @param roomId the room ID
|
||||
*/
|
||||
public void clearRoomSourceStackCache(int roomId) {
|
||||
String prefix = roomId + ":";
|
||||
sourceStacksByTriggerKey.keySet().removeIf(key -> key.startsWith(prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached source-stack lookups.
|
||||
*/
|
||||
public void clearAllSourceStackCache() {
|
||||
sourceStacksByTriggerKey.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all execution-related caches for a specific room.
|
||||
* @param roomId the room ID
|
||||
*/
|
||||
public void clearRoomExecutionCaches(int roomId) {
|
||||
clearRoomRecursionDepth(roomId);
|
||||
clearRoomRateLimiters(roomId);
|
||||
clearRoomSourceStackCache(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all execution-related caches.
|
||||
*/
|
||||
public void clearAllExecutionCaches() {
|
||||
clearAllRecursionDepth();
|
||||
eventRateLimiters.clear();
|
||||
clearAllSourceStackCache();
|
||||
clearUnseenCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear room ban for a specific room.
|
||||
* Should be called when a room is unloaded.
|
||||
* @param roomId the room ID
|
||||
*/
|
||||
public void clearRoomBan(int roomId) {
|
||||
@@ -1165,7 +1326,6 @@ public final class WiredEngine {
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() >= banExpiry) {
|
||||
// Ban expired, remove it
|
||||
bannedRooms.remove(roomId);
|
||||
return false;
|
||||
}
|
||||
@@ -1174,10 +1334,9 @@ public final class WiredEngine {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ban wired execution in a room for WIRED_BAN_DURATION_MS.
|
||||
* Sends alerts to all users in the room and a scripter alert to staff.
|
||||
* Ban wired execution in a room.
|
||||
* @param roomId the room ID
|
||||
* @param room the room object (for sending alerts)
|
||||
* @param room the room object
|
||||
*/
|
||||
private void banRoom(int roomId, Room room, WiredEvent.Type eventType, int eventCount) {
|
||||
long banExpiry = System.currentTimeMillis() + WIRED_BAN_DURATION_MS;
|
||||
@@ -1216,9 +1375,9 @@ public final class WiredEngine {
|
||||
|
||||
/**
|
||||
* Check if an event should be rate-limited.
|
||||
* If rate limit exceeded, bans the room and sends alerts.
|
||||
* Uses a soft limiter only, without banning rooms.
|
||||
* @param roomId the room ID
|
||||
* @param room the room object (for sending alerts if banned)
|
||||
* @param room the room object
|
||||
* @param eventType the event type
|
||||
* @return true if the event should be blocked due to rate limiting
|
||||
*/
|
||||
@@ -1347,20 +1506,19 @@ public final class WiredEngine {
|
||||
private static final class EventRateTracker {
|
||||
private long windowStart;
|
||||
private int eventCount;
|
||||
private boolean banned;
|
||||
private boolean warned;
|
||||
|
||||
EventRateTracker(long now) {
|
||||
this.windowStart = now;
|
||||
this.eventCount = 1;
|
||||
this.banned = false;
|
||||
this.warned = false;
|
||||
}
|
||||
|
||||
synchronized void recordEvent(long now) {
|
||||
// Reset window if expired
|
||||
if (now - windowStart > RATE_LIMIT_WINDOW_MS) {
|
||||
windowStart = now;
|
||||
eventCount = 1;
|
||||
// Don't reset banned here - room ban is checked separately
|
||||
warned = false;
|
||||
} else {
|
||||
eventCount++;
|
||||
}
|
||||
@@ -1370,13 +1528,9 @@ public final class WiredEngine {
|
||||
return eventCount > MAX_EVENTS_PER_WINDOW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is the first time we've hit the limit (to trigger ban).
|
||||
* Returns true only once per suppression window.
|
||||
*/
|
||||
synchronized boolean shouldBan(long now) {
|
||||
if (eventCount > MAX_EVENTS_PER_WINDOW && !banned) {
|
||||
banned = true;
|
||||
if (eventCount > MAX_EVENTS_PER_WINDOW && !warned) {
|
||||
warned = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -57,6 +57,14 @@ import java.util.ArrayDeque;
|
||||
* <li>{@code wired.engine.debug} - Verbose logging</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>Migration Strategy:</h3>
|
||||
* <ol>
|
||||
* <li>Set {@code wired.engine.enabled=true} to run both engines in parallel</li>
|
||||
* <li>Test thoroughly to ensure identical behavior</li>
|
||||
* <li>Set {@code wired.engine.exclusive=true} to disable legacy engine</li>
|
||||
* <li>Full migration complete - WiredManager is now the only wired engine</li>
|
||||
* </ol>
|
||||
*
|
||||
* @see WiredEngine
|
||||
* @see WiredEvents
|
||||
*/
|
||||
@@ -140,8 +148,8 @@ public final class WiredManager {
|
||||
LOGGER.warn("wired.engine.enabled / wired.engine.exclusive are now compatibility-only flags. WiredManager runs as the exclusive engine runtime.");
|
||||
}
|
||||
|
||||
LOGGER.info("Wired Manager initialized - exclusive runtime active, maxSteps: {}, debug: {}",
|
||||
maxSteps, debug);
|
||||
LOGGER.info("Wired Manager initialized - enabled: {}, exclusive runtime active, maxSteps: {}, debug: {}",
|
||||
enabled, maxSteps, debug);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,6 +173,7 @@ public final class WiredManager {
|
||||
if (engine != null) {
|
||||
engine.clearUnseenCache();
|
||||
engine.clearAllDiagnostics();
|
||||
engine.clearAllExecutionCaches();
|
||||
}
|
||||
|
||||
initialized = false;
|
||||
@@ -300,6 +309,18 @@ public final class WiredManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a wired event using the new engine when the source trigger item is already known.
|
||||
* Used by timed wired to avoid scanning unrelated stacks.
|
||||
*/
|
||||
private static boolean handleEventForSourceItem(WiredEvent event, HabboItem sourceItem) {
|
||||
if (!isEnabled() || engine == null || event == null || sourceItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return engine.handleEventForSourceItem(event, sourceItem.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger when a user walks onto furniture.
|
||||
*/
|
||||
@@ -491,24 +512,24 @@ public final class WiredManager {
|
||||
* Trigger a timer tick.
|
||||
*/
|
||||
public static boolean triggerTimerTick(Room room, HabboItem timerItem) {
|
||||
if (!isEnabled() || room == null) {
|
||||
if (!isEnabled() || room == null || timerItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WiredEvent event = WiredEvents.timerTick(room, timerItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, timerItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a periodic timer.
|
||||
*/
|
||||
public static boolean triggerTimerRepeat(Room room, HabboItem timerItem) {
|
||||
if (!isEnabled() || room == null) {
|
||||
if (!isEnabled() || room == null || timerItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WiredEvent event = WiredEvents.timerRepeat(room, timerItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, timerItem);
|
||||
}
|
||||
|
||||
public static boolean triggerClockCounter(Room room, HabboItem counterItem) {
|
||||
@@ -517,31 +538,31 @@ public final class WiredManager {
|
||||
}
|
||||
|
||||
WiredEvent event = WiredEvents.clockCounter(room, counterItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, counterItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a long periodic timer.
|
||||
*/
|
||||
public static boolean triggerTimerRepeatLong(Room room, HabboItem timerItem) {
|
||||
if (!isEnabled() || room == null) {
|
||||
if (!isEnabled() || room == null || timerItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WiredEvent event = WiredEvents.timerRepeatLong(room, timerItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, timerItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a short periodic timer.
|
||||
*/
|
||||
public static boolean triggerTimerRepeatShort(Room room, HabboItem timerItem) {
|
||||
if (!isEnabled() || room == null) {
|
||||
if (!isEnabled() || room == null || timerItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WiredEvent event = WiredEvents.timerRepeatShort(room, timerItem);
|
||||
return handleEvent(event);
|
||||
return handleEventForSourceItem(event, timerItem);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -712,11 +733,20 @@ public final class WiredManager {
|
||||
* Call this when wired items are added/removed/moved.
|
||||
*/
|
||||
public static void invalidateRoom(Room room) {
|
||||
if (stackIndex != null && room != null) {
|
||||
if (room == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stackIndex != null) {
|
||||
stackIndex.invalidateAll(room);
|
||||
if (debugEnabled) {
|
||||
LOGGER.info("[Wired] Cache invalidated for room {}", room.getId());
|
||||
}
|
||||
}
|
||||
|
||||
if (engine != null) {
|
||||
engine.clearRoomExecutionCaches(room.getId());
|
||||
}
|
||||
|
||||
if (debugEnabled) {
|
||||
LOGGER.info("[Wired] Cache invalidated for room {}", room.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,13 +757,25 @@ public final class WiredManager {
|
||||
if (stackIndex != null && room != null && tile != null) {
|
||||
stackIndex.invalidate(room, tile);
|
||||
}
|
||||
|
||||
if (engine != null && room != null) {
|
||||
engine.clearRoomSourceStackCache(room.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the wired index for a room.
|
||||
*/
|
||||
public static void rebuildRoom(Room room) {
|
||||
if (stackIndex != null && room != null) {
|
||||
if (room == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine != null) {
|
||||
engine.clearRoomExecutionCaches(room.getId());
|
||||
}
|
||||
|
||||
if (stackIndex != null) {
|
||||
stackIndex.rebuild(room);
|
||||
}
|
||||
}
|
||||
@@ -851,11 +893,14 @@ public final class WiredManager {
|
||||
*/
|
||||
public static void unregisterRoomTickables(Room room) {
|
||||
WiredTickService.getInstance().unregisterRoom(room);
|
||||
|
||||
if (room != null) {
|
||||
room.getFurniVariableManager().clearTransientAssignments();
|
||||
room.getRoomVariableManager().clearTransientAssignments();
|
||||
}
|
||||
|
||||
if (engine != null && room != null) {
|
||||
engine.clearRoomExecutionCaches(room.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -935,9 +980,9 @@ public final class WiredManager {
|
||||
if (item instanceof InteractionWiredEffect && !(item instanceof WiredEffectTriggerStacks)) {
|
||||
InteractionWiredEffect effect = (InteractionWiredEffect) item;
|
||||
WiredEvent event = WiredEvent.builder(WiredEvent.Type.CUSTOM, room)
|
||||
.actor(roomUnit)
|
||||
.callStackDepth(callStackDepth)
|
||||
.build();
|
||||
.actor(roomUnit)
|
||||
.callStackDepth(callStackDepth)
|
||||
.build();
|
||||
WiredContext ctx = new WiredContext(event, effect, DefaultWiredServices.getInstance(), new WiredState(100));
|
||||
effect.execute(ctx);
|
||||
effect.setCooldown(millis);
|
||||
@@ -1197,4 +1242,3 @@ public final class WiredManager {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,133 +9,110 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Centralized tick service for all wired timing operations.
|
||||
* <p>
|
||||
* This service runs a single 50ms tick loop that processes all registered
|
||||
* {@link WiredTickable} items across all rooms. This replaces the old
|
||||
* per-room 500ms cycle approach and provides:
|
||||
* </p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>Higher resolution timing (50ms vs 500ms)</li>
|
||||
* <li>Centralized management - single thread for all rooms</li>
|
||||
* <li>Proper room lifecycle handling</li>
|
||||
* <li>Efficient registration/unregistration</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>Architecture:</h3>
|
||||
* <pre>
|
||||
* WiredTickService (singleton)
|
||||
* └── ScheduledExecutorService (50ms tick)
|
||||
* └── For each room with tickables:
|
||||
* └── For each WiredTickable:
|
||||
* └── onWiredTick(room, currentTime)
|
||||
* </pre>
|
||||
*
|
||||
* <h3>Thread Safety:</h3>
|
||||
* All collections are thread-safe. The tick loop catches and logs exceptions
|
||||
* to prevent one bad item from crashing the entire service.
|
||||
*
|
||||
* @see WiredTickable
|
||||
* <p>This version keeps a single global tick clock, but distributes room processing
|
||||
* across multiple single-threaded shard workers. A room is always processed on the
|
||||
* same shard, preserving in-room order while preventing one heavy room from delaying
|
||||
* all other rooms.</p>
|
||||
*/
|
||||
public final class WiredTickService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(WiredTickService.class);
|
||||
|
||||
/** Default tick interval in milliseconds */
|
||||
public static final int DEFAULT_TICK_INTERVAL_MS = 50;
|
||||
|
||||
/** Minimum allowed tick interval (prevents CPU overload) */
|
||||
public static final int MIN_TICK_INTERVAL_MS = 10;
|
||||
|
||||
/** Maximum allowed tick interval */
|
||||
public static final int MAX_TICK_INTERVAL_MS = 500;
|
||||
|
||||
/** Singleton instance */
|
||||
public static final int DEFAULT_WORKER_COUNT = Math.max(2, Math.min(8, Runtime.getRuntime().availableProcessors()));
|
||||
public static final int MIN_WORKER_COUNT = 1;
|
||||
public static final int MAX_WORKER_COUNT = 32;
|
||||
|
||||
public static final long SLOW_TICKABLE_THRESHOLD_MS = 100L;
|
||||
public static final long SLOW_ROOM_THRESHOLD_MS = 50L;
|
||||
public static final long SLOW_SHARD_THRESHOLD_MS = 250L;
|
||||
|
||||
private static volatile WiredTickService instance;
|
||||
|
||||
/** The configured tick interval in milliseconds */
|
||||
private int tickIntervalMs = DEFAULT_TICK_INTERVAL_MS;
|
||||
|
||||
/** Whether debug logging is enabled */
|
||||
private boolean debugEnabled = false;
|
||||
|
||||
/** Thread priority for the tick service */
|
||||
private int threadPriority = Thread.NORM_PRIORITY + 1;
|
||||
private int workerCount = DEFAULT_WORKER_COUNT;
|
||||
|
||||
/**
|
||||
* Global tick counter - increments every tick.
|
||||
* All repeaters use this to stay synchronized.
|
||||
* Repeaters fire when (tickCount * tickIntervalMs) % repeatTime == 0
|
||||
*/
|
||||
private volatile long tickCount = 0;
|
||||
/** Global logical tick counter shared by every shard. */
|
||||
private final AtomicLong tickCount = new AtomicLong(0);
|
||||
|
||||
/** The scheduled executor for the tick loop */
|
||||
private ScheduledExecutorService scheduler;
|
||||
/** Schedules the global logical ticks. */
|
||||
private ScheduledExecutorService coordinator;
|
||||
|
||||
/** The scheduled future for the tick task */
|
||||
private ScheduledFuture<?> tickTask;
|
||||
/** One single-thread executor per shard, preserving order inside the shard. */
|
||||
private ExecutorService[] shardExecutors;
|
||||
|
||||
/** Highest logical tick requested for each shard. */
|
||||
private AtomicLong[] shardRequestedTicks;
|
||||
|
||||
/** Last logical tick fully processed by each shard. */
|
||||
private AtomicLong[] shardProcessedTicks;
|
||||
|
||||
/** Whether a shard worker loop is currently scheduled/running. */
|
||||
private AtomicBoolean[] shardScheduled;
|
||||
|
||||
/** Map of room ID to set of registered tickables */
|
||||
private final ConcurrentHashMap<Integer, Set<WiredTickable>> roomTickables;
|
||||
|
||||
/** Whether the service is running */
|
||||
private final AtomicBoolean running;
|
||||
|
||||
/**
|
||||
* Private constructor for singleton.
|
||||
*/
|
||||
private WiredTickService() {
|
||||
this.roomTickables = new ConcurrentHashMap<>();
|
||||
this.running = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads configuration from emulator settings.
|
||||
*/
|
||||
private void loadConfiguration() {
|
||||
// Load tick interval
|
||||
int configuredInterval = Emulator.getConfig().getInt("wired.tick.interval.ms", DEFAULT_TICK_INTERVAL_MS);
|
||||
this.tickIntervalMs = Math.max(MIN_TICK_INTERVAL_MS, Math.min(MAX_TICK_INTERVAL_MS, configuredInterval));
|
||||
|
||||
if (configuredInterval != this.tickIntervalMs) {
|
||||
LOGGER.warn("wired.tick.interval.ms value {} is out of range [{}-{}], using {}",
|
||||
configuredInterval, MIN_TICK_INTERVAL_MS, MAX_TICK_INTERVAL_MS, this.tickIntervalMs);
|
||||
LOGGER.warn(
|
||||
"wired.tick.interval.ms value {} is out of range [{}-{}], using {}",
|
||||
configuredInterval,
|
||||
MIN_TICK_INTERVAL_MS,
|
||||
MAX_TICK_INTERVAL_MS,
|
||||
this.tickIntervalMs
|
||||
);
|
||||
}
|
||||
|
||||
// Load debug flag
|
||||
this.debugEnabled = Emulator.getConfig().getBoolean("wired.tick.debug", false);
|
||||
|
||||
// Load thread priority
|
||||
int configuredPriority = Emulator.getConfig().getInt("wired.tick.thread.priority", Thread.NORM_PRIORITY + 1);
|
||||
this.threadPriority = Math.max(Thread.MIN_PRIORITY, Math.min(Thread.MAX_PRIORITY, configuredPriority));
|
||||
|
||||
int configuredWorkers = Emulator.getConfig().getInt("wired.tick.workers", DEFAULT_WORKER_COUNT);
|
||||
this.workerCount = Math.max(MIN_WORKER_COUNT, Math.min(MAX_WORKER_COUNT, configuredWorkers));
|
||||
|
||||
if (configuredWorkers != this.workerCount) {
|
||||
LOGGER.warn(
|
||||
"wired.tick.workers value {} is out of range [{}-{}], using {}",
|
||||
configuredWorkers,
|
||||
MIN_WORKER_COUNT,
|
||||
MAX_WORKER_COUNT,
|
||||
this.workerCount
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configured tick interval in milliseconds.
|
||||
*
|
||||
* @return the tick interval
|
||||
*/
|
||||
public int getTickIntervalMs() {
|
||||
return tickIntervalMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if debug logging is enabled.
|
||||
*
|
||||
* @return true if debug is enabled
|
||||
*/
|
||||
public boolean isDebugEnabled() {
|
||||
return debugEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance.
|
||||
*
|
||||
* @return the WiredTickService instance
|
||||
*/
|
||||
public int getWorkerCount() {
|
||||
return workerCount;
|
||||
}
|
||||
|
||||
public static WiredTickService getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (WiredTickService.class) {
|
||||
@@ -147,120 +124,135 @@ public final class WiredTickService {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the tick service.
|
||||
* <p>
|
||||
* Should be called during emulator startup after WiredManager.initialize().
|
||||
* </p>
|
||||
*/
|
||||
public synchronized void start() {
|
||||
if (running.get()) {
|
||||
LOGGER.warn("WiredTickService already running");
|
||||
return;
|
||||
}
|
||||
|
||||
// Load configuration from emulator settings
|
||||
loadConfiguration();
|
||||
|
||||
LOGGER.info("Starting WiredTickService with {}ms tick interval (debug={}, priority={})...",
|
||||
tickIntervalMs, debugEnabled, threadPriority);
|
||||
LOGGER.info(
|
||||
"Starting WiredTickService with {}ms tick interval (workers={}, debug={}, priority={})...",
|
||||
tickIntervalMs,
|
||||
workerCount,
|
||||
debugEnabled,
|
||||
threadPriority
|
||||
);
|
||||
|
||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "WiredTickService");
|
||||
this.coordinator = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "WiredTickCoordinator");
|
||||
t.setDaemon(true);
|
||||
t.setPriority(threadPriority);
|
||||
return t;
|
||||
});
|
||||
|
||||
this.tickTask = scheduler.scheduleAtFixedRate(
|
||||
this::tick,
|
||||
tickIntervalMs,
|
||||
tickIntervalMs,
|
||||
TimeUnit.MILLISECONDS
|
||||
this.shardExecutors = new ExecutorService[workerCount];
|
||||
this.shardRequestedTicks = new AtomicLong[workerCount];
|
||||
this.shardProcessedTicks = new AtomicLong[workerCount];
|
||||
this.shardScheduled = new AtomicBoolean[workerCount];
|
||||
|
||||
for (int i = 0; i < workerCount; i++) {
|
||||
final int shardIndex = i;
|
||||
this.shardExecutors[i] = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "WiredTickShard-" + shardIndex);
|
||||
t.setDaemon(true);
|
||||
t.setPriority(threadPriority);
|
||||
return t;
|
||||
});
|
||||
this.shardRequestedTicks[i] = new AtomicLong(0L);
|
||||
this.shardProcessedTicks[i] = new AtomicLong(0L);
|
||||
this.shardScheduled[i] = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
this.tickCount.set(0L);
|
||||
running.set(true);
|
||||
|
||||
this.coordinator.scheduleAtFixedRate(
|
||||
() -> {
|
||||
try {
|
||||
dispatchTick();
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error("WiredTickService fatal coordinator error", t);
|
||||
}
|
||||
},
|
||||
tickIntervalMs,
|
||||
tickIntervalMs,
|
||||
TimeUnit.MILLISECONDS
|
||||
);
|
||||
|
||||
running.set(true);
|
||||
LOGGER.info("WiredTickService started successfully");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the tick service.
|
||||
* <p>
|
||||
* Should be called during emulator shutdown.
|
||||
* </p>
|
||||
*/
|
||||
public synchronized void stop() {
|
||||
if (!running.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.info("Stopping WiredTickService...");
|
||||
|
||||
running.set(false);
|
||||
|
||||
if (tickTask != null) {
|
||||
tickTask.cancel(false);
|
||||
tickTask = null;
|
||||
}
|
||||
|
||||
if (scheduler != null) {
|
||||
scheduler.shutdown();
|
||||
if (coordinator != null) {
|
||||
coordinator.shutdown();
|
||||
try {
|
||||
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
scheduler.shutdownNow();
|
||||
if (!coordinator.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
coordinator.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
scheduler.shutdownNow();
|
||||
coordinator.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
scheduler = null;
|
||||
coordinator = null;
|
||||
}
|
||||
|
||||
if (shardExecutors != null) {
|
||||
for (ExecutorService executor : shardExecutors) {
|
||||
if (executor != null) {
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
for (ExecutorService executor : shardExecutors) {
|
||||
if (executor == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
executor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shardExecutors = null;
|
||||
shardRequestedTicks = null;
|
||||
shardProcessedTicks = null;
|
||||
shardScheduled = null;
|
||||
|
||||
roomTickables.clear();
|
||||
LOGGER.info("WiredTickService stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the service is running.
|
||||
*
|
||||
* @return true if running
|
||||
*/
|
||||
public boolean isRunning() {
|
||||
return running.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a tickable item with the service.
|
||||
* <p>
|
||||
* The item will start receiving {@link WiredTickable#onWiredTick} calls
|
||||
* on the next tick cycle.
|
||||
* </p>
|
||||
*
|
||||
* @param room the room the item is in
|
||||
* @param tickable the tickable item
|
||||
*/
|
||||
public void register(Room room, WiredTickable tickable) {
|
||||
if (room == null || tickable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = room.getId();
|
||||
Set<WiredTickable> tickables = roomTickables.computeIfAbsent(
|
||||
roomId,
|
||||
k -> ConcurrentHashMap.newKeySet()
|
||||
);
|
||||
Set<WiredTickable> tickables = roomTickables.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet());
|
||||
|
||||
if (tickables.add(tickable)) {
|
||||
tickable.onRegistered(room, System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a tickable item from the service.
|
||||
*
|
||||
* @param room the room the item was in
|
||||
* @param tickable the tickable item
|
||||
*/
|
||||
public void unregister(Room room, WiredTickable tickable) {
|
||||
if (room == null || tickable == null) {
|
||||
return;
|
||||
@@ -274,19 +266,12 @@ public final class WiredTickService {
|
||||
tickable.onUnregistered(room);
|
||||
}
|
||||
|
||||
// Clean up empty sets
|
||||
if (tickables.isEmpty()) {
|
||||
roomTickables.remove(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a tickable by ID.
|
||||
*
|
||||
* @param roomId the room ID
|
||||
* @param tickableId the tickable item ID
|
||||
*/
|
||||
public void unregister(int roomId, int tickableId) {
|
||||
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
||||
|
||||
@@ -308,14 +293,6 @@ public final class WiredTickService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters all tickables for a room.
|
||||
* <p>
|
||||
* Should be called when a room is unloaded.
|
||||
* </p>
|
||||
*
|
||||
* @param room the room
|
||||
*/
|
||||
public void unregisterRoom(Room room) {
|
||||
if (room == null) {
|
||||
return;
|
||||
@@ -324,18 +301,25 @@ public final class WiredTickService {
|
||||
Set<WiredTickable> tickables = roomTickables.remove(room.getId());
|
||||
|
||||
if (tickables != null) {
|
||||
for (WiredTickable tickable : tickables) {
|
||||
tickable.onUnregistered(room);
|
||||
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
|
||||
for (WiredTickable tickable : snapshot) {
|
||||
try {
|
||||
if (tickable != null) {
|
||||
tickable.onUnregistered(room);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error(
|
||||
"Error unregistering tickable {} from room {}",
|
||||
tickable != null ? tickable.getId() : -1,
|
||||
room.getId(),
|
||||
t
|
||||
);
|
||||
}
|
||||
}
|
||||
LOGGER.debug("Unregistered {} tickables from room {}", tickables.size(), room.getId());
|
||||
LOGGER.debug("Unregistered {} tickables from room {}", snapshot.length, room.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all timers in a room.
|
||||
*
|
||||
* @param room the room
|
||||
*/
|
||||
public void resetRoomTimers(Room room) {
|
||||
if (room == null) {
|
||||
return;
|
||||
@@ -344,119 +328,198 @@ public final class WiredTickService {
|
||||
Set<WiredTickable> tickables = roomTickables.get(room.getId());
|
||||
|
||||
if (tickables != null) {
|
||||
for (WiredTickable tickable : tickables) {
|
||||
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
|
||||
for (WiredTickable tickable : snapshot) {
|
||||
try {
|
||||
tickable.resetTimer();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error resetting timer for tickable {} in room {}",
|
||||
tickable.getId(), room.getId(), e);
|
||||
if (tickable != null) {
|
||||
tickable.resetTimer();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error(
|
||||
"Error resetting timer for tickable {} in room {}",
|
||||
tickable != null ? tickable.getId() : -1,
|
||||
room.getId(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of registered tickables for a room.
|
||||
*
|
||||
* @param roomId the room ID
|
||||
* @return the count
|
||||
*/
|
||||
public int getTickableCount(int roomId) {
|
||||
Set<WiredTickable> tickables = roomTickables.get(roomId);
|
||||
return tickables != null ? tickables.size() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total count of registered tickables across all rooms.
|
||||
*
|
||||
* @return the total count
|
||||
*/
|
||||
public int getTotalTickableCount() {
|
||||
return roomTickables.values().stream()
|
||||
.mapToInt(Set::size)
|
||||
.sum();
|
||||
return roomTickables.values().stream().mapToInt(Set::size).sum();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of rooms with registered tickables.
|
||||
*
|
||||
* @return the room count
|
||||
*/
|
||||
public int getActiveRoomCount() {
|
||||
return roomTickables.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* The main tick loop.
|
||||
* <p>
|
||||
* Called at the configured interval by the scheduler. Processes all registered tickables
|
||||
* across all rooms.
|
||||
* </p>
|
||||
*/
|
||||
private void tick() {
|
||||
public long getTickCount() {
|
||||
return tickCount.get();
|
||||
}
|
||||
|
||||
private void dispatchTick() {
|
||||
if (!running.get() || Emulator.isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment global tick counter
|
||||
tickCount++;
|
||||
long currentTick = tickCount.incrementAndGet();
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
int tickablesProcessed = 0;
|
||||
for (int shardIndex = 0; shardIndex < workerCount; shardIndex++) {
|
||||
shardRequestedTicks[shardIndex].set(currentTick);
|
||||
scheduleShardIfNeeded(shardIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleShardIfNeeded(int shardIndex) {
|
||||
if (!running.get() || shardExecutors == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shardScheduled[shardIndex].compareAndSet(false, true)) {
|
||||
shardExecutors[shardIndex].execute(() -> runShardLoop(shardIndex));
|
||||
}
|
||||
}
|
||||
|
||||
private void runShardLoop(int shardIndex) {
|
||||
try {
|
||||
while (running.get() && !Emulator.isShuttingDown) {
|
||||
long nextTick = shardProcessedTicks[shardIndex].get() + 1L;
|
||||
long requestedTick = shardRequestedTicks[shardIndex].get();
|
||||
|
||||
if (nextTick > requestedTick) {
|
||||
break;
|
||||
}
|
||||
|
||||
processShardTick(shardIndex, nextTick);
|
||||
shardProcessedTicks[shardIndex].set(nextTick);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error("Fatal error in WiredTick shard {}", shardIndex, t);
|
||||
} finally {
|
||||
shardScheduled[shardIndex].set(false);
|
||||
if (running.get() && shardProcessedTicks[shardIndex].get() < shardRequestedTicks[shardIndex].get()) {
|
||||
scheduleShardIfNeeded(shardIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processShardTick(int shardIndex, long currentTick) {
|
||||
long shardStart = System.currentTimeMillis();
|
||||
int processedTickables = 0;
|
||||
int processedRooms = 0;
|
||||
|
||||
for (Map.Entry<Integer, Set<WiredTickable>> entry : roomTickables.entrySet()) {
|
||||
int roomId = entry.getKey();
|
||||
Set<WiredTickable> tickables = entry.getValue();
|
||||
|
||||
if (tickables.isEmpty()) {
|
||||
if (getShardIndex(roomId) != shardIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Set<WiredTickable> tickables = entry.getValue();
|
||||
if (tickables == null || tickables.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the room - skip if not loaded
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
|
||||
if (room == null || !room.isLoaded()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if room is empty (optimization)
|
||||
if (room.getCurrentHabbos().isEmpty() && room.getCurrentBots().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process each tickable
|
||||
for (WiredTickable tickable : tickables) {
|
||||
long roomStart = System.currentTimeMillis();
|
||||
WiredTickable[] snapshot = tickables.toArray(new WiredTickable[0]);
|
||||
if (snapshot.length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
processedRooms++;
|
||||
|
||||
for (WiredTickable tickable : snapshot) {
|
||||
long tickableStart = System.currentTimeMillis();
|
||||
|
||||
if (tickable == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify item still belongs to this room
|
||||
if (tickable.getRoomId() != roomId) {
|
||||
// Item moved to another room, unregister it
|
||||
tickables.remove(tickable);
|
||||
unregister(roomId, tickable.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pass global tick count - all tickables see the same counter
|
||||
// This keeps repeaters with the same interval perfectly synchronized
|
||||
tickable.onWiredTick(room, tickCount, tickIntervalMs);
|
||||
tickablesProcessed++;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error in wired tick for tickable {} in room {}: {}",
|
||||
tickable.getId(), roomId, e.getMessage(), e);
|
||||
tickable.onWiredTick(room, currentTick, tickIntervalMs);
|
||||
processedTickables++;
|
||||
|
||||
long tickableDuration = System.currentTimeMillis() - tickableStart;
|
||||
if (tickableDuration > SLOW_TICKABLE_THRESHOLD_MS) {
|
||||
LOGGER.warn(
|
||||
"Slow wired tickable: shard={}, room={}, tick={}, tickableId={}, class={}, took={}ms",
|
||||
shardIndex,
|
||||
roomId,
|
||||
currentTick,
|
||||
tickable.getId(),
|
||||
tickable.getClass().getName(),
|
||||
tickableDuration
|
||||
);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
long tickableDuration = System.currentTimeMillis() - tickableStart;
|
||||
LOGGER.error(
|
||||
"Error in wired tick for tickable {} in room {} after {}ms",
|
||||
tickable.getId(),
|
||||
roomId,
|
||||
tickableDuration,
|
||||
t
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
long roomDuration = System.currentTimeMillis() - roomStart;
|
||||
if (roomDuration > SLOW_ROOM_THRESHOLD_MS) {
|
||||
LOGGER.warn(
|
||||
"Slow wired room tick: shard={}, room={}, tick={}, tickables={}, took={}ms",
|
||||
shardIndex,
|
||||
roomId,
|
||||
currentTick,
|
||||
snapshot.length,
|
||||
roomDuration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Debug logging if enabled
|
||||
if (debugEnabled && tickablesProcessed > 0) {
|
||||
LOGGER.debug("Wired tick #{} completed: {} tickables processed in {}ms",
|
||||
tickCount, tickablesProcessed, System.currentTimeMillis() - startTime);
|
||||
long shardDuration = System.currentTimeMillis() - shardStart;
|
||||
if (shardDuration > SLOW_SHARD_THRESHOLD_MS) {
|
||||
LOGGER.warn(
|
||||
"Slow wired shard tick: shard={}, tick={}, rooms={}, tickables={}, took={}ms",
|
||||
shardIndex,
|
||||
currentTick,
|
||||
processedRooms,
|
||||
processedTickables,
|
||||
shardDuration
|
||||
);
|
||||
}
|
||||
|
||||
if (debugEnabled && processedTickables > 0) {
|
||||
LOGGER.debug(
|
||||
"Wired shard tick completed: shard={}, tick={}, rooms={}, tickables={}, took={}ms",
|
||||
shardIndex,
|
||||
currentTick,
|
||||
processedRooms,
|
||||
processedTickables,
|
||||
shardDuration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current global tick count.
|
||||
*
|
||||
* @return the tick count
|
||||
*/
|
||||
public long getTickCount() {
|
||||
return tickCount;
|
||||
private int getShardIndex(int roomId) {
|
||||
return Math.floorMod(roomId, workerCount);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -76,13 +76,15 @@ public class BuildersClubPlaceRoomItemEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(BuildersClubRoomSupport.VIRTUAL_OWNER_ID, baseItem, 0, 0, (extraData != null && !extraData.isEmpty()) ? extraData : catalogItem.getExtradata());
|
||||
HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(placementUserId, baseItem, 0, 0, (extraData != null && !extraData.isEmpty()) ? extraData : catalogItem.getExtradata());
|
||||
|
||||
if (item == null) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode));
|
||||
return;
|
||||
}
|
||||
|
||||
item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
|
||||
|
||||
FurnitureMovementError error = room.canPlaceFurnitureAt(item, this.client.getHabbo(), tile, rotation);
|
||||
|
||||
if (!error.equals(FurnitureMovementError.NONE)) {
|
||||
|
||||
+3
-1
@@ -66,13 +66,15 @@ public class BuildersClubPlaceWallItemEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(BuildersClubRoomSupport.VIRTUAL_OWNER_ID, baseItem, 0, 0, (extraData != null && !extraData.isEmpty()) ? extraData : catalogItem.getExtradata());
|
||||
HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(placementUserId, baseItem, 0, 0, (extraData != null && !extraData.isEmpty()) ? extraData : catalogItem.getExtradata());
|
||||
|
||||
if (item == null) {
|
||||
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.INVALID_MOVE.errorCode));
|
||||
return;
|
||||
}
|
||||
|
||||
item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
|
||||
|
||||
FurnitureMovementError error = room.placeWallFurniAt(item, wallPosition, this.client.getHabbo());
|
||||
|
||||
if (!error.equals(FurnitureMovementError.NONE)) {
|
||||
|
||||
+193
-41
@@ -44,15 +44,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
LOGGER.error("DEBUG GIFT: entered CatalogBuyItemAsGiftEvent.handle()");
|
||||
|
||||
if (Emulator.getIntUnixTimestamp() - this.client.getHabbo().getHabboStats().lastGiftTimestamp >= CatalogManager.PURCHASE_COOLDOWN) {
|
||||
this.client.getHabbo().getHabboStats().lastGiftTimestamp = Emulator.getIntUnixTimestamp();
|
||||
|
||||
if (ShutdownEmulator.timestamp > 0) {
|
||||
LOGGER.error("DEBUG GIFT: emulator closing");
|
||||
this.client.sendResponse(new HotelWillCloseInMinutesComposer((ShutdownEmulator.timestamp - Emulator.getIntUnixTimestamp()) / 60));
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.client.getHabbo().getHabboStats().isPurchasingFurniture) {
|
||||
LOGGER.error("DEBUG GIFT: isPurchasingFurniture already true");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
} else {
|
||||
@@ -60,7 +65,6 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
int pageId = this.packet.readInt();
|
||||
int itemId = this.packet.readInt();
|
||||
String extraData = this.packet.readString();
|
||||
@@ -71,14 +75,22 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
int ribbonId = this.packet.readInt();
|
||||
boolean showName = this.packet.readBoolean();
|
||||
|
||||
LOGGER.error(
|
||||
"DEBUG GIFT: pageId={}, itemId={}, extraData={}, username={}, spriteId={}, color={}, ribbonId={}, showName={}, message={}",
|
||||
pageId, itemId, extraData, username, spriteId, color, ribbonId, showName, message
|
||||
);
|
||||
|
||||
int userId = 0;
|
||||
|
||||
if (!Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId) && !Emulator.getGameEnvironment().getCatalogManager().giftFurnis.containsKey(spriteId)) {
|
||||
if (!Emulator.getGameEnvironment().getCatalogManager().giftWrappers.containsKey(spriteId)
|
||||
&& !Emulator.getGameEnvironment().getCatalogManager().giftFurnis.containsKey(spriteId)) {
|
||||
LOGGER.error("DEBUG GIFT: invalid spriteId for gift wrapper/furni -> {}", spriteId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!GiftConfigurationComposer.BOX_TYPES.contains(color) || !GiftConfigurationComposer.RIBBON_TYPES.contains(ribbonId)) {
|
||||
LOGGER.error("DEBUG GIFT: invalid color/ribbon -> color={}, ribbonId={}", color, ribbonId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
@@ -89,10 +101,12 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
Integer iItemId = Emulator.getGameEnvironment().getCatalogManager().giftWrappers.get(spriteId);
|
||||
|
||||
if (iItemId == null)
|
||||
if (iItemId == null) {
|
||||
iItemId = Emulator.getGameEnvironment().getCatalogManager().giftFurnis.get(spriteId);
|
||||
}
|
||||
|
||||
if (iItemId == null) {
|
||||
LOGGER.error("DEBUG GIFT: iItemId null for spriteId={}", spriteId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
@@ -100,9 +114,15 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
Item giftItem = Emulator.getGameEnvironment().getItemManager().getItem(iItemId);
|
||||
|
||||
if (giftItem == null) {
|
||||
giftItem = Emulator.getGameEnvironment().getItemManager().getItem((Integer) Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray()[Emulator.getRandom().nextInt(Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size())]);
|
||||
LOGGER.error("DEBUG GIFT: direct giftItem null, trying random fallback. iItemId={}", iItemId);
|
||||
giftItem = Emulator.getGameEnvironment().getItemManager().getItem(
|
||||
(Integer) Emulator.getGameEnvironment().getCatalogManager().giftFurnis.values().toArray()[
|
||||
Emulator.getRandom().nextInt(Emulator.getGameEnvironment().getCatalogManager().giftFurnis.size())
|
||||
]
|
||||
);
|
||||
|
||||
if (giftItem == null) {
|
||||
LOGGER.error("DEBUG GIFT: fallback giftItem also null");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
@@ -112,6 +132,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
|
||||
|
||||
if (habbo == null) {
|
||||
LOGGER.error("DEBUG GIFT: target user not online, checking DB -> {}", username);
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE username = ?")) {
|
||||
statement.setString(1, username);
|
||||
|
||||
@@ -128,6 +149,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
|
||||
if (userId == 0) {
|
||||
LOGGER.error("DEBUG GIFT: receiver not found -> {}", username);
|
||||
this.client.sendResponse(new GiftReceiverNotFoundComposer());
|
||||
return;
|
||||
}
|
||||
@@ -135,11 +157,17 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().catalogPages.get(pageId);
|
||||
|
||||
if (page == null) {
|
||||
LOGGER.error("DEBUG GIFT: page null -> {}", pageId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (page.getRank() > this.client.getHabbo().getHabboInfo().getRank().getId() || !page.isEnabled() || !page.isVisible()) {
|
||||
LOGGER.error("DEBUG GIFT: page access denied. pageRank={}, userRank={}, enabled={}, visible={}",
|
||||
page.getRank(),
|
||||
this.client.getHabbo().getHabboInfo().getRank().getId(),
|
||||
page.isEnabled(),
|
||||
page.isVisible());
|
||||
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
|
||||
return;
|
||||
}
|
||||
@@ -147,17 +175,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
CatalogItem item = page.getCatalogItem(itemId);
|
||||
|
||||
if (item == null) {
|
||||
LOGGER.error("DEBUG GIFT: catalog item null -> {}", itemId);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.isClubOnly() && !this.client.getHabbo().getHabboStats().hasActiveClub()) {
|
||||
LOGGER.error("DEBUG GIFT: item requires club -> itemId={}", itemId);
|
||||
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.REQUIRES_CLUB));
|
||||
return;
|
||||
}
|
||||
|
||||
for (Item baseItem : item.getBaseItems()) {
|
||||
if (!baseItem.allowGift()) {
|
||||
LOGGER.error("DEBUG GIFT: base item not giftable -> baseItemId={}, name={}", baseItem.getId(), baseItem.getName());
|
||||
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
|
||||
return;
|
||||
}
|
||||
@@ -165,6 +196,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
if (item.isLimited()) {
|
||||
if (item.getLimitedStack() == item.getLimitedSells()) {
|
||||
LOGGER.error("DEBUG GIFT: LTD sold out -> itemId={}", itemId);
|
||||
this.client.sendResponse(new AlertLimitedSoldOutComposer());
|
||||
return;
|
||||
}
|
||||
@@ -173,7 +205,14 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
int totalCredits = item.getCredits();
|
||||
int totalPoints = item.getPoints();
|
||||
|
||||
if(totalCredits > this.client.getHabbo().getHabboInfo().getCredits() || totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType())) {
|
||||
if (totalCredits > this.client.getHabbo().getHabboInfo().getCredits()
|
||||
|| totalPoints > this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType())) {
|
||||
LOGGER.error("DEBUG GIFT: not enough currency. creditsNeeded={}, creditsHave={}, pointsNeeded={}, pointsHave={}, pointsType={}",
|
||||
totalCredits,
|
||||
this.client.getHabbo().getHabboInfo().getCredits(),
|
||||
totalPoints,
|
||||
this.client.getHabbo().getHabboInfo().getCurrencyAmount(item.getPointsType()),
|
||||
item.getPointsType());
|
||||
this.client.sendResponse(new AlertPurchaseUnavailableComposer(AlertPurchaseUnavailableComposer.ILLEGAL));
|
||||
return;
|
||||
}
|
||||
@@ -181,23 +220,34 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
CatalogLimitedConfiguration limitedConfiguration = null;
|
||||
int limitedStack = 0;
|
||||
int limitedNumber = 0;
|
||||
|
||||
if (item.isLimited()) {
|
||||
if (Emulator.getGameEnvironment().getCatalogManager().getLimitedConfig(item).available() == 0) {
|
||||
LOGGER.error("DEBUG GIFT: LTD available=0 -> itemId={}", itemId);
|
||||
this.client.sendResponse(new AlertLimitedSoldOutComposer());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check daily LTD limits for the buyer (sender of the gift)
|
||||
if (Emulator.getConfig().getBoolean("hotel.catalog.ltd.limit.enabled")) {
|
||||
int ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.total");
|
||||
if (this.client.getHabbo().getHabboStats().totalLtds() >= ltdLimit) {
|
||||
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total").replace("%itemname%", item.getBaseItems().iterator().next().getFullName()).replace("%limit%", ltdLimit + ""));
|
||||
LOGGER.error("DEBUG GIFT: sender reached daily total LTD limit");
|
||||
this.client.getHabbo().alert(
|
||||
Emulator.getTexts().getValue("error.catalog.buy.limited.daily.total")
|
||||
.replace("%itemname%", item.getBaseItems().iterator().next().getFullName())
|
||||
.replace("%limit%", ltdLimit + "")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ltdLimit = Emulator.getConfig().getInt("hotel.purchase.ltd.limit.daily.item");
|
||||
if (this.client.getHabbo().getHabboStats().totalLtds(item.getId()) >= ltdLimit) {
|
||||
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item").replace("%itemname%", item.getBaseItems().iterator().next().getFullName()).replace("%limit%", ltdLimit + ""));
|
||||
LOGGER.error("DEBUG GIFT: sender reached daily LTD item limit");
|
||||
this.client.getHabbo().alert(
|
||||
Emulator.getTexts().getValue("error.catalog.buy.limited.daily.item")
|
||||
.replace("%itemname%", item.getBaseItems().iterator().next().getFullName())
|
||||
.replace("%limit%", ltdLimit + "")
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -210,8 +260,6 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
|
||||
limitedNumber = limitedConfiguration.getNumber();
|
||||
limitedStack = limitedConfiguration.getTotalSet();
|
||||
|
||||
// Log the LTD purchase for daily limits
|
||||
this.client.getHabbo().getHabboStats().addLtdLog(item.getId(), Emulator.getIntUnixTimestamp());
|
||||
}
|
||||
|
||||
@@ -229,6 +277,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) as c FROM users_badges WHERE user_id = ? AND badge_code LIKE ?")) {
|
||||
statement.setInt(1, userId);
|
||||
statement.setString(2, baseItem.getName());
|
||||
|
||||
try (ResultSet rSet = statement.executeQuery()) {
|
||||
if (rSet.next()) {
|
||||
c = rSet.getInt("c");
|
||||
@@ -244,17 +293,20 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
|
||||
if (badgeFound) {
|
||||
LOGGER.error("DEBUG GIFT: receiver already has badge");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.ALREADY_HAVE_BADGE));
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.getAmount() > 1 || item.getBaseItems().size() > 1) {
|
||||
LOGGER.error("DEBUG GIFT: unsupported multi amount/baseItems. amount={}, baseItems={}", item.getAmount(), item.getBaseItems().size());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
|
||||
for (Item baseItem : item.getBaseItems()) {
|
||||
if (item.getItemAmount(baseItem.getId()) > 1) {
|
||||
LOGGER.error("DEBUG GIFT: unsupported item amount > 1 for baseItemId={}", baseItem.getId());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
}
|
||||
@@ -278,37 +330,88 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
badgeFound = true;
|
||||
}
|
||||
} else if (item.getName().startsWith("rentable_bot_")) {
|
||||
LOGGER.error("DEBUG GIFT: rentable bot gifts not supported");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
} else if (Item.isPet(baseItem)) {
|
||||
LOGGER.error("DEBUG GIFT: pet gifts not supported");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
|
||||
return;
|
||||
} else {
|
||||
if (baseItem.getInteractionType().getType() == InteractionTrophy.class || baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class) {
|
||||
if (baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class && habbo != null && !habbo.getClient().getHabbo().getInventory().getBadgesComponent().hasBadge(extraData)) {
|
||||
ScripterManager.scripterDetected(habbo.getClient(), Emulator.getTexts().getValue("scripter.warning.catalog.badge_display").replace("%username%", habbo.getClient().getHabbo().getHabboInfo().getUsername()).replace("%badge%", extraData));
|
||||
if (baseItem.getInteractionType().getType() == InteractionTrophy.class
|
||||
|| baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class) {
|
||||
if (baseItem.getInteractionType().getType() == InteractionBadgeDisplay.class
|
||||
&& habbo != null
|
||||
&& !habbo.getClient().getHabbo().getInventory().getBadgesComponent().hasBadge(extraData)) {
|
||||
ScripterManager.scripterDetected(
|
||||
habbo.getClient(),
|
||||
Emulator.getTexts().getValue("scripter.warning.catalog.badge_display")
|
||||
.replace("%username%", habbo.getClient().getHabbo().getHabboInfo().getUsername())
|
||||
.replace("%badge%", extraData)
|
||||
);
|
||||
extraData = "UMAD";
|
||||
}
|
||||
|
||||
extraData = this.client.getHabbo().getHabboInfo().getUsername() + (char) 9 + Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "-" + (Calendar.getInstance().get(Calendar.MONTH) + 1) + "-" + Calendar.getInstance().get(Calendar.YEAR) + (char) 9 + extraData;
|
||||
extraData = this.client.getHabbo().getHabboInfo().getUsername()
|
||||
+ (char) 9
|
||||
+ Calendar.getInstance().get(Calendar.DAY_OF_MONTH)
|
||||
+ "-"
|
||||
+ (Calendar.getInstance().get(Calendar.MONTH) + 1)
|
||||
+ "-"
|
||||
+ Calendar.getInstance().get(Calendar.YEAR)
|
||||
+ (char) 9
|
||||
+ extraData;
|
||||
}
|
||||
|
||||
if (baseItem.getInteractionType().getType() == InteractionTeleport.class || baseItem.getInteractionType().getType() == InteractionTeleportTile.class) {
|
||||
HabboItem teleportOne = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
|
||||
HabboItem teleportTwo = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
|
||||
if (baseItem.getInteractionType().getType() == InteractionTeleport.class
|
||||
|| baseItem.getInteractionType().getType() == InteractionTeleportTile.class) {
|
||||
|
||||
HabboItem teleportOne = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
|
||||
HabboItem teleportTwo = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
|
||||
|
||||
if (teleportOne == null || teleportTwo == null) {
|
||||
LOGGER.error("DEBUG GIFT: teleport creation failed. baseItemId={}, teleportOneNull={}, teleportTwoNull={}",
|
||||
baseItem.getId(), teleportOne == null, teleportTwo == null);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
Emulator.getGameEnvironment().getItemManager().insertTeleportPair(teleportOne.getId(), teleportTwo.getId());
|
||||
itemsList.add(teleportOne);
|
||||
itemsList.add(teleportTwo);
|
||||
|
||||
} else if (baseItem.getInteractionType().getType() == InteractionHopper.class) {
|
||||
HabboItem hopper = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedNumber, limitedNumber, extraData);
|
||||
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedNumber, limitedNumber, extraData);
|
||||
|
||||
Emulator.getGameEnvironment().getItemManager().insertHopper(hopper);
|
||||
if (habboItem == null) {
|
||||
LOGGER.error("DEBUG GIFT: hopper creation failed. baseItemId={}", baseItem.getId());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
itemsList.add(hopper);
|
||||
} else if (baseItem.getInteractionType().getType() == InteractionGuildFurni.class || baseItem.getInteractionType().getType() == InteractionGuildGate.class) {
|
||||
InteractionGuildFurni habboItem = (InteractionGuildFurni) Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
|
||||
Emulator.getGameEnvironment().getItemManager().insertHopper(habboItem);
|
||||
itemsList.add(habboItem);
|
||||
|
||||
} else if (baseItem.getInteractionType().getType() == InteractionGuildFurni.class
|
||||
|| baseItem.getInteractionType().getType() == InteractionGuildGate.class) {
|
||||
HabboItem createdItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
|
||||
|
||||
if (createdItem == null) {
|
||||
LOGGER.error("DEBUG GIFT: guild item creation failed. baseItemId={}", baseItem.getId());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(createdItem instanceof InteractionGuildFurni)) {
|
||||
LOGGER.error("DEBUG GIFT: created guild item has wrong class -> {}", createdItem.getClass().getName());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
InteractionGuildFurni habboItem = (InteractionGuildFurni) createdItem;
|
||||
habboItem.setExtradata("");
|
||||
habboItem.needsUpdate(true);
|
||||
|
||||
int guildId;
|
||||
try {
|
||||
guildId = Integer.parseInt(extraData);
|
||||
@@ -317,15 +420,24 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
Emulator.getThreading().run(habboItem);
|
||||
Emulator.getGameEnvironment().getGuildManager().setGuild(habboItem, guildId);
|
||||
itemsList.add(habboItem);
|
||||
} else {
|
||||
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(0, baseItem, limitedStack, limitedNumber, extraData);
|
||||
HabboItem habboItem = Emulator.getGameEnvironment().getItemManager().createItem(userId, baseItem, limitedStack, limitedNumber, extraData);
|
||||
|
||||
if (habboItem == null) {
|
||||
LOGGER.error("DEBUG GIFT: normal item creation failed. baseItemId={}, baseItemName={}", baseItem.getId(), baseItem.getName());
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
itemsList.add(habboItem);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOGGER.error("DEBUG GIFT: avatar_effect not supported");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
this.client.sendResponse(new GenericAlertComposer(Emulator.getTexts().getValue("error.catalog.buy.not_yet")));
|
||||
return;
|
||||
@@ -333,48 +445,85 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder giftData = new StringBuilder(itemsList.size() + "\t");
|
||||
|
||||
for (HabboItem i : itemsList) {
|
||||
giftData.append(i.getId()).append("\t");
|
||||
}
|
||||
|
||||
giftData.append(color).append("\t").append(ribbonId).append("\t").append(showName ? "1" : "0").append("\t").append(message.replace("\t", "")).append("\t").append(this.client.getHabbo().getHabboInfo().getUsername()).append("\t").append(this.client.getHabbo().getHabboInfo().getLook());
|
||||
|
||||
HabboItem gift = Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, giftData.toString(), 0, 0);
|
||||
|
||||
if (gift == null) {
|
||||
if (itemsList.isEmpty()) {
|
||||
LOGGER.error("DEBUG GIFT: itemsList empty before giftData");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder giftData = new StringBuilder(itemsList.size() + "\t");
|
||||
|
||||
for (HabboItem i : itemsList) {
|
||||
if (i == null) {
|
||||
LOGGER.error("DEBUG GIFT: null HabboItem detected inside itemsList");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
giftData.append(i.getId()).append("\t");
|
||||
}
|
||||
|
||||
giftData.append(color)
|
||||
.append("\t")
|
||||
.append(ribbonId)
|
||||
.append("\t")
|
||||
.append(showName ? "1" : "0")
|
||||
.append("\t")
|
||||
.append(message.replace("\t", ""))
|
||||
.append("\t")
|
||||
.append(this.client.getHabbo().getHabboInfo().getUsername())
|
||||
.append("\t")
|
||||
.append(this.client.getHabbo().getHabboInfo().getLook());
|
||||
|
||||
HabboItem gift = Emulator.getGameEnvironment().getItemManager().createGift(username, giftItem, giftData.toString(), 0, 0);
|
||||
|
||||
if (gift == null) {
|
||||
LOGGER.error("DEBUG GIFT: createGift returned null");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark limited items as sold in the database to prevent duplication after catalog reload
|
||||
if (limitedConfiguration != null) {
|
||||
for (HabboItem itm : itemsList) {
|
||||
if (itm == null) {
|
||||
LOGGER.error("DEBUG GIFT: null item before limitedSold()");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
limitedConfiguration.limitedSold(item.getId(), this.client.getHabbo(), itm);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.client.getHabbo().getHabboInfo().getId() != userId) {
|
||||
AchievementManager.progressAchievement(this.client.getHabbo(), Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftGiver"));
|
||||
AchievementManager.progressAchievement(
|
||||
this.client.getHabbo(),
|
||||
Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftGiver")
|
||||
);
|
||||
}
|
||||
|
||||
if (habbo != null) {
|
||||
habbo.getClient().sendResponse(new AddHabboItemComposer(gift));
|
||||
habbo.getClient().getHabbo().getInventory().getItemsComponent().addItem(gift);
|
||||
habbo.getClient().sendResponse(new InventoryRefreshComposer());
|
||||
|
||||
THashMap<String, String> keys = new THashMap<>();
|
||||
keys.put("display", "BUBBLE");
|
||||
keys.put("image", "${image.library.url}notifications/gift.gif");
|
||||
keys.put("message", Emulator.getTexts().getValue("generic.gift.received.anonymous"));
|
||||
|
||||
if (showName) {
|
||||
keys.put("message", Emulator.getTexts().getValue("generic.gift.received").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
|
||||
keys.put("message", Emulator.getTexts().getValue("generic.gift.received")
|
||||
.replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
|
||||
}
|
||||
|
||||
habbo.getClient().sendResponse(new BubbleAlertComposer(BubbleAlertKeys.RECEIVED_BADGE.key, keys));
|
||||
}
|
||||
|
||||
if (this.client.getHabbo().getHabboInfo().getId() != userId) {
|
||||
AchievementManager.progressAchievement(userId, Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftReceiver"));
|
||||
AchievementManager.progressAchievement(
|
||||
userId,
|
||||
Emulator.getGameEnvironment().getAchievementManager().getAchievement("GiftReceiver")
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) {
|
||||
@@ -382,6 +531,7 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
this.client.getHabbo().giveCredits(-totalCredits);
|
||||
}
|
||||
}
|
||||
|
||||
if (totalPoints > 0) {
|
||||
if (item.getPointsType() == 0 && !this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_PIXELS)) {
|
||||
this.client.getHabbo().givePixels(-totalPoints);
|
||||
@@ -390,15 +540,17 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.error("DEBUG GIFT: success sending PurchaseOKComposer");
|
||||
this.client.sendResponse(new PurchaseOKComposer(item));
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Exception caught", e);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Exception caught", e);
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
} finally {
|
||||
this.client.getHabbo().getHabboStats().isPurchasingFurniture = false;
|
||||
}
|
||||
} else {
|
||||
LOGGER.error("DEBUG GIFT: cooldown blocked purchase");
|
||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ public class RoomPlaceItemEvent extends MessageHandler {
|
||||
trackedUserId = this.client.getHabbo().getHabboInfo().getId();
|
||||
}
|
||||
|
||||
item.setUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
|
||||
item.setVirtualUserId(BuildersClubRoomSupport.VIRTUAL_OWNER_ID);
|
||||
BuildersClubRoomSupport.trackPlacedItem(item.getId(), trackedUserId, room.getId());
|
||||
|
||||
BuildersClubRoomSupport.SyncResult syncResult = BuildersClubRoomSupport.syncRoom(room);
|
||||
|
||||
+45
-3
@@ -26,9 +26,16 @@ public class RoomUserWalkEvent extends MessageHandler {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RoomUserWalkEvent.class);
|
||||
public static final String CONTROL_KEY = "control";
|
||||
|
||||
private static final String WALK_FLOOD_COUNT_KEY = "__walkFloodCount";
|
||||
private static final String WALK_FLOOD_WINDOW_KEY = "__walkFloodWindow";
|
||||
private static final String WALK_LAST_X_KEY = "__walkLastX";
|
||||
private static final String WALK_LAST_Y_KEY = "__walkLastY";
|
||||
|
||||
private static final int MAX_WALKS_PER_SECOND = 15;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return Emulator.getConfig().getInt("pathfinder.click.delay", 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -37,8 +44,43 @@ public class RoomUserWalkEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
int x = this.packet.readInt(); // Position X
|
||||
int y = this.packet.readInt(); // Position Y
|
||||
int x = this.packet.readInt();
|
||||
int y = this.packet.readInt();
|
||||
|
||||
RoomUnit unit = this.client.getHabbo().getRoomUnit();
|
||||
if (unit != null) {
|
||||
long now = System.currentTimeMillis();
|
||||
Object windowObj = unit.getCacheable().get(WALK_FLOOD_WINDOW_KEY);
|
||||
Object countObj = unit.getCacheable().get(WALK_FLOOD_COUNT_KEY);
|
||||
|
||||
long windowStart = (windowObj instanceof Long) ? (Long) windowObj : 0L;
|
||||
int count = (countObj instanceof Integer) ? (Integer) countObj : 0;
|
||||
|
||||
if (now - windowStart > 1000) {
|
||||
// New 1-second window
|
||||
windowStart = now;
|
||||
count = 0;
|
||||
}
|
||||
|
||||
count++;
|
||||
unit.getCacheable().put(WALK_FLOOD_WINDOW_KEY, windowStart);
|
||||
unit.getCacheable().put(WALK_FLOOD_COUNT_KEY, count);
|
||||
|
||||
if (count > MAX_WALKS_PER_SECOND) {
|
||||
unit.getCacheable().put(WALK_LAST_X_KEY, x);
|
||||
unit.getCacheable().put(WALK_LAST_Y_KEY, y);
|
||||
return;
|
||||
}
|
||||
|
||||
Object lastX = unit.getCacheable().get(WALK_LAST_X_KEY);
|
||||
Object lastY = unit.getCacheable().get(WALK_LAST_Y_KEY);
|
||||
if (lastX != null && lastY != null) {
|
||||
x = (Integer) lastX;
|
||||
y = (Integer) lastY;
|
||||
unit.getCacheable().remove(WALK_LAST_X_KEY);
|
||||
unit.getCacheable().remove(WALK_LAST_Y_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
Habbo habbo = getControlledHabbo();
|
||||
if (habbo == null) {
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user